Up to date Version

This commit is contained in:
Olai Vike Bøe 2024-09-10 11:53:00 +02:00
parent bc9fcda6aa
commit 2db08a537c
12 changed files with 552 additions and 779 deletions

3
.gitignore vendored
View file

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

119
app.py
View file

@ -6,6 +6,7 @@ import hashlib
import os import os
import random import random
import string import string
import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from flask_socketio import SocketIO, emit, join_room, leave_room from flask_socketio import SocketIO, emit, join_room, leave_room
@ -19,8 +20,21 @@ logging.basicConfig(filename='app.log', level=logging.DEBUG,
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24) app.config['SECRET_KEY'] = None # Initially set to None
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
def load_secret_key():
secret_key_path = 'secret_key.json'
if not os.path.exists(secret_key_path):
secret_key = os.urandom(24)
with open(secret_key_path, 'w') as key_file:
json.dump({'SECRET_KEY': secret_key.hex()}, key_file)
else:
with open(secret_key_path, 'r') as key_file:
secret_key = bytes.fromhex(json.load(key_file)['SECRET_KEY'])
return secret_key
app.config['SECRET_KEY'] = load_secret_key()
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///W:/site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'cdn' app.config['UPLOAD_FOLDER'] = 'cdn'
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
@ -30,12 +44,12 @@ migrate = Migrate(app, db)
# Load encryption key # Load encryption key
def load_key(): def load_key():
return open("secret.key", "rb").read() return open(r"W:\secret.key", "rb").read()
# Ensure the key file exists # Ensure the key file exists
if not os.path.exists("secret.key"): if not os.path.exists(r"W:\secret.key"):
key = Fernet.generate_key() key = Fernet.generate_key()
with open("secret.key", "wb") as key_file: with open(r"secret.key", "wb") as key_file:
key_file.write(key) key_file.write(key)
key = load_key() key = load_key()
@ -149,17 +163,31 @@ def register():
username = request.form['username'].lower() username = request.form['username'].lower()
password = request.form['password'] password = request.form['password']
hashed_password = sha256_password_hash(password) hashed_password = sha256_password_hash(password)
new_user = User(username=username, password=hashed_password) profile_picture = "default_profile_picture.png" # Set default profile picture
new_user = User(username=username, password=hashed_password, profile_picture=profile_picture)
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}") logger.info(f"New user registered: {username}")
return redirect(url_for('login'))
# Automatically log the user in after registration
session['username'] = new_user.username
session['user_id'] = new_user.id
session['is_admin'] = new_user.is_admin
token = generate_token(new_user.id)
response = make_response(redirect(url_for('dashboard')))
expires = datetime.now(timezone.utc) + timedelta(days=7)
response.set_cookie('token', token, httponly=True, expires=expires)
logger.info(f"User logged in: {username} after registration")
return response
except Exception as e: except Exception as e:
logger.error(f"Error registering user {username}: {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')
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
if 'username' in session: if 'username' in session:
@ -300,8 +328,12 @@ 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))
profile_picture = user.profile_picture if user.profile_picture else 'default_profile_picture.png'
for friend in friend_users:
friend.profile_picture = friend.profile_picture if friend.profile_picture else 'default_profile_picture.png'
logger.info(f"User {session['username']} accessed dashboard") 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, groups=groups, is_admin=user.is_admin) 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, groups=groups, is_admin=user.is_admin, profile_picture=profile_picture)
return redirect(url_for('login')) return redirect(url_for('login'))
@app.route('/chat/<int:friend_id>') @app.route('/chat/<int:friend_id>')
@ -401,7 +433,7 @@ def get_messages(friend_id):
{ {
'id': msg.id, 'id': msg.id,
'sender': msg.sender.username, 'sender': msg.sender.username,
'sender_profile_picture': f'/cdn/{msg.sender.profile_picture}' if msg.sender.profile_picture else None, 'sender_profile_picture': f'{msg.sender.profile_picture}' if msg.sender.profile_picture else None,
'content': cipher.decrypt(msg.content.encode()).decode() if msg.content_type == 'text' else msg.content, 'content': cipher.decrypt(msg.content.encode()).decode() if msg.content_type == 'text' else msg.content,
'content_type': msg.content_type, 'content_type': msg.content_type,
'timestamp': msg.timestamp.strftime("%Y-%m-%d %H:%M:%S") 'timestamp': msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")
@ -782,7 +814,7 @@ def handle_connect():
user = User.query.filter_by(username=session['username']).first() user = User.query.filter_by(username=session['username']).first()
if user: if user:
user.online = True user.online = True
user.last_activity = datetime.utcnow() user.last_activity = datetime.now(timezone.utc)
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") logger.info(f"User {session['username']} connected")
@ -797,14 +829,60 @@ def handle_disconnect():
emit('user_offline', {'username': user.username}, broadcast=True) emit('user_offline', {'username': user.username}, broadcast=True)
logger.info(f"User {session['username']} disconnected") logger.info(f"User {session['username']} disconnected")
@socketio.on('send_message')
def handle_send_message(data):
sender = session['username']
receiver_id = data['receiver_id']
message = data['message']
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Save the message in the database
new_message = Message(sender_id=User.query.filter_by(username=sender).first().id, receiver_id=receiver_id, content=message, timestamp=timestamp)
db.session.add(new_message)
db.session.commit()
# Emit the message to the receiver if they are online and not in the same chat
receiver = User.query.get(receiver_id)
if receiver.online:
emit('new_message', {
'sender': sender,
'message': message,
'timestamp': timestamp,
'receiver_id': receiver_id
}, room=receiver.username)
@socketio.on('send_group_message')
def handle_send_group_message(data):
sender = session['username']
group_id = data['group_id']
message = data['message']
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Save the message in the database
new_group_message = GroupMessage(sender_id=User.query.filter_by(username=sender).first().id, group_id=group_id, content=message, timestamp=timestamp)
db.session.add(new_group_message)
db.session.commit()
# Emit the message to the group if they are online and not in the same group chat
group_members = GroupMember.query.filter_by(group_id=group_id).all()
for member in group_members:
user = User.query.get(member.user_id)
if user.username != sender and user.online:
emit('new_group_message', {
'sender': sender,
'message': message,
'timestamp': timestamp,
'group_id': group_id
}, room=user.username)
@socketio.on('join') @socketio.on('join')
def handle_join(data): def on_join(data):
username = data['username'] username = data['username']
join_room(username) join_room(username)
logger.info(f"User {username} joined room {username}") logger.info(f"User {username} joined room {username}")
@socketio.on('leave') @socketio.on('leave')
def handle_leave(data): def on_leave(data):
username = data['username'] username = data['username']
leave_room(username) leave_room(username)
logger.info(f"User {username} left room {username}") logger.info(f"User {username} left room {username}")
@ -823,23 +901,6 @@ def handle_leave_group(data):
leave_room(f"group_{group_id}") leave_room(f"group_{group_id}")
logger.info(f"User {username} left group room {group_id}") logger.info(f"User {username} left group room {group_id}")
@socketio.on('user_activity')
def handle_user_activity():
if 'username' in session:
user = User.query.filter_by(username=session['username']).first()
if user:
user.last_activity = datetime.utcnow()
db.session.commit()
def set_users_to_idle():
with app.app_context():
idle_threshold = datetime.utcnow() - timedelta(minutes=5)
users = User.query.filter(User.online == True, User.last_activity < idle_threshold).all()
for user in users:
user.online = False
db.session.commit()
emit('user_idle', {'username': user.username}, broadcast=True)
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
db.create_all() db.create_all()

BIN
instance/site.db.lnk Normal file

Binary file not shown.

View file

@ -2,9 +2,11 @@ Flask==3.0.3
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy==3.1.1
Flask-SocketIO==5.3.6 Flask-SocketIO==5.3.6
Flask-Migrate==4.0.7 Flask-Migrate==4.0.7
Flask-Session==0.8.0
cryptography==42.0.8 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 PyJWT==2.8.0
Pillow==10.3.0 Pillow==10.3.0
redis==5.0.6

View file

@ -1,21 +1,5 @@
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
const socket = io(); const socket = io();
const md = window.markdownit();
function requestNotificationPermission() {
if (Notification.permission !== "granted") {
Notification.requestPermission();
}
}
function showNotification(title, body) {
if (Notification.permission === "granted") {
new Notification(title, { body });
}
}
requestNotificationPermission();
socket.emit('join', { username: username }); socket.emit('join', { username: username });
document.title = `${friendUsername}`; document.title = `${friendUsername}`;
@ -23,352 +7,9 @@ document.addEventListener('DOMContentLoaded', (event) => {
const messagesList = document.getElementById('messages'); const messagesList = document.getElementById('messages');
const imageOverlay = document.getElementById('image-overlay'); const imageOverlay = document.getElementById('image-overlay');
const overlayImage = document.getElementById('overlay-image'); 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;
}
function formatLocalTime(utcTimestamp) {
const date = new Date(utcTimestamp);
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const timeFormatter = { hour: '2-digit', minute: '2-digit' };
const dateFormatter = { year: 'numeric', month: '2-digit', day: '2-digit' };
let formattedTime;
if (localDate >= today) {
formattedTime = `Today at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
} else if (localDate >= yesterday) {
formattedTime = `Yesterday at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
} else {
formattedTime = `${localDate.toLocaleDateString(undefined, dateFormatter)} at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
}
return formattedTime;
}
function isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|bmp|svg)$/i.test(url);
}
function appendMessage(msg, showUsername, senderProfilePicture) {
const currentTimestamp = new Date(msg.timestamp).getTime();
previousSender = msg.sender;
previousTimestamp = currentTimestamp;
const newMessage = document.createElement('div');
newMessage.classList.add('message');
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
if (msg.sender === username) {
usernameElement.classList.add('message-myself-sender');
}
usernameElement.innerHTML = `
<img src="/cdn/${senderProfilePicture}" alt="/cdn/${msg.sender}" class="profile-picture">
<strong>${msg.sender}</strong>
<div class="message-sender-timestamp">${formatLocalTime(msg.timestamp)}</div>`;
messagesList.appendChild(usernameElement);
}
if (msg.content_type === 'text') {
if (isImageUrl(msg.content)) {
newMessage.innerHTML = `<img src="${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else {
newMessage.innerHTML = `${md.render(msg.content)}<div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
}
} else if (msg.content_type === 'file') {
const isImage = isImageUrl(msg.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(msg.content);
if (isImage) {
newMessage.innerHTML = `<img src="/cdn/${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${msg.content}" type="video/mp4"></video><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else {
newMessage.innerHTML = `<a href="/cdn/${msg.content}" target="_blank">Download File</a><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
}
let previousSender = null; let previousSender = null;
let previousTimestamp = 0; let previousTimestamp = 0;
socket.on('new_message', function(data) {
// Check if the message is for the current chat
if (friendUsername !== data.sender && friendId != data.sender_id) {
return;
}
showNotification('New Message', `You have received a new message from ${data.sender}`);
const currentTimestamp = new Date(data.timestamp).getTime();
const showUsername = previousSender !== data.sender || shouldShowUsername(previousTimestamp, currentTimestamp);
previousSender = data.sender;
previousTimestamp = currentTimestamp;
const messageElement = document.createElement('div');
messageElement.classList.add('message');
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.dataset.messageId = data.id;
newMessage.dataset.messageText = data.content;
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
const timestampFormatted = formatLocalTime(data.timestamp);
// Include the profile picture if available
const profilePicture = data.sender_profile_picture ? `<img src="/cdn/${data.sender_profile_picture}" alt="${data.sender}" class="profile-picture enhanceable-image">` : '';
usernameElement.innerHTML = `${profilePicture} <strong>${data.sender}</strong><div class="message-sender-timestamp">${timestampFormatted}</div>`;
messagesList.appendChild(usernameElement);
}
if (data.content_type === 'text') {
if (isImageUrl(data.content)) {
newMessage.innerHTML = `<img src="${data.content}" alt="Image" class="enhanceable-image" style="max-width: 300px; max-height: 300px;" /><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
} else {
newMessage.innerHTML = `${md.render(data.content)}<div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
}
} else if (data.content_type === 'file') {
const isImage = isImageUrl(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: 300px; max-height: 300px;" /><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<video controls style="max-width: 300px; max-height: 300px;"><source src="/cdn/${data.content}" type="video/mp4"></video><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
} else {
newMessage.innerHTML = `<a href="/cdn/${data.content}" target="_blank">Download File</a><div class="timestamp">${formatLocalTime(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 = `${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');
if (msg.sender === username) {
usernameElement.classList.add('message-myself-sender');
}
// Include the profile picture if available
const profilePicture = msg.sender_profile_picture ? `<img src="${msg.sender_profile_picture}" alt="${msg.sender}" class="profile-picture enhanceable-image">` : '';
usernameElement.innerHTML = `${profilePicture} <strong>${msg.sender}</strong><div class="message-sender-timestamp">${formatLocalTime(msg.timestamp)}</div>`;
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') {
if (isImageUrl(msg.content)) {
messageElement.innerHTML = `<img src="${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else {
messageElement.innerHTML = `${md.render(msg.content)}<div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
}
} else if (msg.content_type === 'file') {
const isImage = isImageUrl(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">${formatLocalTime(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">${formatLocalTime(msg.timestamp)}</div>`;
} else {
messageElement.innerHTML = `<a href="/cdn/${msg.content}" target="_blank">Download File</a><div class="timestamp">${formatLocalTime(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');
const timestampFormatted = formatLocalTime(timestamp);
const profilePictureUrl = `/cdn/${profilePicture}`; // Correct reference
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${username}" class="profile-picture enhanceable-image"> <strong>${username}</strong><div class="message-sender-timestamp">${timestampFormatted}</div>`;
messagesList.appendChild(usernameElement);
}
if (content) {
newMessage.dataset.messageText = content;
if (isImageUrl(content)) {
newMessage.innerHTML = `<img src="${content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${formatLocalTime(timestamp)}</div>`;
} else {
newMessage.innerHTML = `${md.render(content)}<div class="timestamp">${formatLocalTime(timestamp)}</div>`;
}
}
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const isImage = /\.(jpg|jpeg|png|gif|bmp|svg)$/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">${formatLocalTime(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">${formatLocalTime(timestamp)}</div>`;
} else {
newMessage.innerHTML = `<a href="${URL.createObjectURL(file)}" target="_blank">Download File</a><div class="timestamp">${formatLocalTime(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() { function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests'); const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section'); const friendRequestsSection = document.getElementById('friend-requests-section');
@ -383,57 +24,115 @@ document.addEventListener('DOMContentLoaded', (event) => {
checkFriendRequests(); checkFriendRequests();
// Image enhancer functionality socket.on('new_message', function (data) {
document.addEventListener('click', function(event) { const currentTimestamp = new Date(data.timestamp).getTime();
const showUsername = previousSender !== data.sender || shouldShowUsername(previousTimestamp, currentTimestamp);
previousSender = data.sender;
previousTimestamp = currentTimestamp;
const profilePicUrl = data.sender_profile_picture ? `${data.sender_profile_picture}` : 'default_profile_picture.png';
appendMessage(messagesList, data, showUsername, profilePicUrl);
});
if (friendId) {
fetch(`/get_messages/${friendId}`)
.then(response => response.json())
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `${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);
const profilePicUrl = msg.sender_profile_picture ? `${msg.sender_profile_picture}` : 'default_profile_picture.png';
appendMessage(messagesList, msg, showUsername, profilePicUrl);
});
scrollToBottom(messagesList);
})
.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) {
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 = '';
// Append the message immediately for the sender
const profilePictureUrl = profilePicture ? `${profilePicture}` : 'default_profile_picture.png';
const messageData = {
sender: username,
content: content,
timestamp: timestamp,
sender_profile_picture: profilePictureUrl,
content_type: fileInput.files.length > 0 ? 'file' : 'text',
id: data.message_id
};
const currentTimestamp = new Date(messageData.timestamp).getTime();
const showUsername = previousSender !== username || shouldShowUsername(previousTimestamp, currentTimestamp);
previousSender = username;
previousTimestamp = currentTimestamp;
appendMessage(messagesList, messageData, showUsername, profilePictureUrl);
} else {
showToast('Message sending failed');
}
}).catch(error => {
showToast('Message sending failed');
console.error('Error sending message:', error);
});
}
});
document.querySelector('input[name="file"]').addEventListener('change', handleFileInputChange);
document.querySelector('.remove-file').addEventListener('click', handleRemoveFile);
}
document.addEventListener('click', function (event) {
if (event.target.classList.contains('enhanceable-image')) { if (event.target.classList.contains('enhanceable-image')) {
overlayImage.src = event.target.src; overlayImage.src = event.target.src;
imageOverlay.style.display = 'flex'; imageOverlay.style.display = 'flex';
} }
}); });
imageOverlay.addEventListener('click', function() { imageOverlay.addEventListener('click', function () {
imageOverlay.style.display = 'none'; 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';
});
}); });

136
static/js/common.js Normal file
View file

@ -0,0 +1,136 @@
// common.js
const md = window.markdownit();
const contextMenu = document.getElementById('context-menu');
let currentMessageId = null;
let currentMessageText = null;
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
function scrollToBottom(element) {
element.scrollTop = element.scrollHeight;
}
function shouldShowUsername(previousTimestamp, currentTimestamp) {
const tenMinutes = 10 * 60 * 1000;
return (currentTimestamp - previousTimestamp) > tenMinutes;
}
function formatLocalTime(utcTimestamp) {
const date = new Date(utcTimestamp);
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const timeFormatter = { hour: '2-digit', minute: '2-digit' };
const dateFormatter = { year: 'numeric', month: '2-digit', day: '2-digit' };
let formattedTime;
if (localDate >= today) {
formattedTime = `Today at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
} else if (localDate >= yesterday) {
formattedTime = `Yesterday at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
} else {
formattedTime = `${localDate.toLocaleDateString(undefined, dateFormatter)} at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
}
return formattedTime;
}
function isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|bmp|svg)$/i.test(url);
}
function appendMessage(messagesList, msg, showUsername, senderProfilePicture) {
const currentTimestamp = new Date(msg.timestamp).getTime();
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.dataset.messageId = msg.id;
newMessage.dataset.messageText = msg.content;
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
if (msg.sender === username) {
usernameElement.classList.add('message-myself-sender');
}
const profilePicUrl = senderProfilePicture ? `/cdn/${senderProfilePicture}` : '/cdn/default_profile_picture.png';
usernameElement.innerHTML = `
<img src="${profilePicUrl}" alt="${msg.sender}" class="profile-picture">
<strong>${msg.sender}</strong>
<div class="message-sender-timestamp">${formatLocalTime(msg.timestamp)}</div>`;
messagesList.appendChild(usernameElement);
}
if (msg.content_type === 'text') {
if (isImageUrl(msg.content)) {
newMessage.innerHTML = `<img src="${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else {
newMessage.innerHTML = `${md.render(msg.content)}<div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
}
} else if (msg.content_type === 'file') {
const isImage = isImageUrl(msg.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(msg.content);
if (isImage) {
newMessage.innerHTML = `<img src="/cdn/${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${msg.content}" type="video/mp4"></video><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
} else {
newMessage.innerHTML = `<a href="/cdn/${msg.content}" target="_blank">Download File</a><div class="timestamp">${formatLocalTime(msg.timestamp)}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom(messagesList);
}
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';
});

View file

@ -12,9 +12,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
setInterval(() => { setInterval(() => {
if (!isConnected) { if (!isConnected) {
if (confirm('Something went wrong, please reload the website')) { window.location.reload();
window.location.reload();
}
} }
isConnected = false; isConnected = false;
checkConnection(); checkConnection();
@ -26,8 +24,6 @@ document.addEventListener('DOMContentLoaded', (event) => {
socket.on('disconnect', () => { socket.on('disconnect', () => {
isConnected = false; isConnected = false;
if (confirm('Something went wrong, please reload the website')) { window.location.reload();
window.location.reload();
}
}); });
}); });

View file

@ -1,6 +1,5 @@
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
const socket = io(); const socket = io();
const md = window.markdownit();
socket.emit('join_group', { group_id: groupId }); socket.emit('join_group', { group_id: groupId });
document.title = `${groupName}`; document.title = `${groupName}`;
@ -9,166 +8,62 @@ document.addEventListener('DOMContentLoaded', (event) => {
const imageOverlay = document.getElementById('image-overlay'); const imageOverlay = document.getElementById('image-overlay');
const overlayImage = document.getElementById('overlay-image'); const overlayImage = document.getElementById('overlay-image');
const contextMenu = document.getElementById('context-menu'); const contextMenu = document.getElementById('context-menu');
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
let currentMessageId = null; let currentMessageId = null;
let currentMessageText = 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;
}
function formatLocalTime(utcTimestamp) {
const date = new Date(utcTimestamp);
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const timeFormatter = { hour: '2-digit', minute: '2-digit' };
const dateFormatter = { year: 'numeric', month: '2-digit', day: '2-digit' };
let formattedTime;
if (localDate >= today) {
formattedTime = `Today at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
} else if (localDate >= yesterday) {
formattedTime = `Yesterday at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
} else {
formattedTime = `${localDate.toLocaleDateString(undefined, dateFormatter)} at ${localDate.toLocaleTimeString(undefined, timeFormatter)}`;
}
return formattedTime;
}
function isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|bmp|svg)$/i.test(url);
}
let previousSender = null; let previousSender = null;
let previousTimestamp = 0; let previousTimestamp = 0;
socket.on('new_group_message', function(data) { 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();
socket.on('new_group_message', function (data) {
const currentTimestamp = new Date(data.timestamp).getTime(); const currentTimestamp = new Date(data.timestamp).getTime();
const showUsername = previousSender !== data.sender || shouldShowUsername(previousTimestamp, currentTimestamp); const showUsername = previousSender !== data.sender || shouldShowUsername(previousTimestamp, currentTimestamp);
previousSender = data.sender; previousSender = data.sender;
previousTimestamp = currentTimestamp; previousTimestamp = currentTimestamp;
const newMessage = document.createElement('div'); const profilePicUrl = data.sender_profile_picture ? `${data.sender_profile_picture}` : 'default_profile_picture.png';
newMessage.classList.add('message'); appendMessage(messagesList, data, showUsername, profilePicUrl);
});
newMessage.dataset.messageId = data.id;
newMessage.dataset.messageText = data.content;
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
const profilePictureUrl = `/cdn/${data.sender_profile_picture}`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${data.sender}" class="profile-picture enhanceable-image"> <strong>${data.sender}</strong><div class="message-sender-timestamp">${formatLocalTime(data.timestamp)}</div>`;
messagesList.appendChild(usernameElement);
}
if (data.content_type === 'text') {
if (isImageUrl(data.content)) {
newMessage.innerHTML = `<img src="${data.content}" alt="Image" class="enhanceable-image" style="max-width: 300px; max-height: 300px;" /><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
} else {
newMessage.innerHTML = `${md.render(data.content)}<div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
}
} else if (data.content_type === 'file') {
const isImage = isImageUrl(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: 300px; max-height: 300px;" /><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<video controls style="max-width: 300px; max-height: 300px;"><source src="/cdn/${data.content}" type="video/mp4"></video><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
} else {
newMessage.innerHTML = `<a href="/cdn/${data.content}" target="_blank">Download File</a><div class="timestamp">${formatLocalTime(data.timestamp)}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
});
if (groupId) { if (groupId) {
fetch(`/get_group_messages/${groupId}`) fetch(`/get_group_messages/${groupId}`)
.then(response => { .then(response => response.json())
if (!response.ok) { .then(data => {
throw new Error(`HTTP error! Status: ${response.status}`); messagesList.innerHTML = '';
} document.getElementById('chat-with').textContent = `${groupName}`;
return response.json(); document.getElementById('send-message-form').dataset.receiver = groupId;
})
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `${groupName}`;
document.getElementById('send-message-form').dataset.receiver = groupId;
data.messages.forEach((msg, index, messages) => { data.messages.forEach((msg, index, messages) => {
const currentTimestamp = new Date(msg.timestamp).getTime(); const currentTimestamp = new Date(msg.timestamp).getTime();
const previousMessage = messages[index - 1]; const previousMessage = messages[index - 1];
const previousTimestamp = previousMessage ? new Date(previousMessage.timestamp).getTime() : 0; const previousTimestamp = previousMessage ? new Date(previousMessage.timestamp).getTime() : 0;
const showUsername = index === 0 || msg.sender !== previousMessage.sender || shouldShowUsername(previousTimestamp, currentTimestamp); const showUsername = index === 0 || msg.sender !== previousMessage.sender || shouldShowUsername(previousTimestamp, currentTimestamp);
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
if (msg.sender === username) {
usernameElement.classList.add('message-myself-sender');
}
const profilePictureUrl = `/cdn/${msg.sender_profile_picture}`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${msg.sender}" class="profile-picture enhanceable-image"> <strong>${msg.sender}</strong><div class="message-sender-timestamp">${formatLocalTime(msg.timestamp)}</div>`;
messagesList.appendChild(usernameElement);
}
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.dataset.messageId = msg.id; const profilePicUrl = msg.sender_profile_picture ? `${msg.sender_profile_picture}` : 'default_profile_picture.png';
messageElement.dataset.messageText = msg.content; appendMessage(messagesList, msg, showUsername, profilePicUrl);
});
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|svg)$/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 group messages:', error);
alert('Error fetching group messages: ' + error.message);
});
scrollToBottom(messagesList);
})
.catch(error => console.error('Error fetching group messages:', error));
} }
const sendMessageForm = document.querySelector('.send-message-form'); const sendMessageForm = document.querySelector('.send-message-form');
if (sendMessageForm) { if (sendMessageForm) {
sendMessageForm.addEventListener('submit', function(event) { sendMessageForm.addEventListener('submit', function (event) {
event.preventDefault(); event.preventDefault();
const receiver = this.dataset.receiver; const receiver = this.dataset.receiver;
const contentInput = this.querySelector('input[name="content"]'); const contentInput = this.querySelector('input[name="content"]');
@ -187,162 +82,61 @@ document.addEventListener('DOMContentLoaded', (event) => {
formData.append('file', fileInput.files[0]); formData.append('file', fileInput.files[0]);
} }
fetch(`/send_group_message/${groupId}`, { fetch(`/send_group_message/${receiver}`, {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => response.json()) }).then(response => response.json())
.then(data => { .then(data => {
if (data.status === 'Message sent') { if (data.status === 'Message sent') {
contentInput.value = ''; contentInput.value = '';
fileInput.value = ''; fileInput.value = '';
document.querySelector('.file-preview').style.display = 'none'; document.querySelector('.file-preview').style.display = 'none';
document.querySelector('.file-preview img').src = ''; document.querySelector('.file-preview img').src = '';
document.querySelector('.file-preview video').style.display = 'none'; document.querySelector('.file-preview video').style.display = 'none';
document.querySelector('.file-preview video').src = ''; document.querySelector('.file-preview video').src = '';
document.querySelector('.file-preview .file-preview-filename').style.display = 'none'; document.querySelector('.file-preview .file-preview-filename').style.display = 'none';
document.querySelector('.file-preview .file-preview-filename').textContent = ''; document.querySelector('.file-preview .file-preview-filename').textContent = '';
// The message will be appended only when the server confirms it // Append the message immediately for the sender
socket.emit('new_group_message', { const profilePictureUrl = profilePicture ? `${profilePicture}` : 'default_profile_picture.png';
group_id: groupId, const messageData = {
sender: username, sender: username,
content: content, content: content,
content_type: 'text', // Adjust this as necessary timestamp: timestamp,
timestamp: timestamp, sender_profile_picture: profilePictureUrl,
id: data.message_id content_type: fileInput.files.length > 0 ? 'file' : 'text',
}); id: data.message_id
} else { };
showToast('Message sending failed');
} const currentTimestamp = new Date(messageData.timestamp).getTime();
}).catch(error => { const showUsername = previousSender !== username || shouldShowUsername(previousTimestamp, currentTimestamp);
showToast('Message sending failed'); previousSender = username;
console.error('Error sending group message:', error); previousTimestamp = currentTimestamp;
});
appendMessage(messagesList, messageData, showUsername, profilePictureUrl);
} else {
showToast('Message sending failed');
}
}).catch(error => {
showToast('Message sending failed');
console.error('Error sending message:', error);
});
} }
}); });
// Handle Shift + Enter for new line document.querySelector('input[name="file"]').addEventListener('change', handleFileInputChange);
const messageInput = document.querySelector('input[name="content"]');
messageInput.addEventListener('keydown', function(event) {
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.value = this.value.substring(0, start) + "\n" + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 1;
}
});
document.querySelector('input[name="file"]').addEventListener('change', function(event) { document.querySelector('.remove-file').addEventListener('click', handleRemoveFile);
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() { document.addEventListener('click', function (event) {
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')) { if (event.target.classList.contains('enhanceable-image')) {
overlayImage.src = event.target.src; overlayImage.src = event.target.src;
imageOverlay.style.display = 'flex'; imageOverlay.style.display = 'flex';
} }
}); });
imageOverlay.addEventListener('click', function() { imageOverlay.addEventListener('click', function () {
imageOverlay.style.display = 'none'; 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';
});
}); });

View file

@ -1,25 +1,58 @@
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
function requestNotificationPermission() { const socket = io();
if (Notification.permission !== "granted") {
Notification.requestPermission(); let isConnected = true;
function checkConnection() {
socket.emit('ping');
}
socket.on('pong', function () {
isConnected = true;
});
setInterval(() => {
if (!isConnected) {
window.location.reload();
} }
isConnected = false;
checkConnection();
}, 3000);
socket.on('connect', () => {
isConnected = true;
});
socket.on('disconnect', () => {
isConnected = false;
window.location.reload();
});
// Function to check if the user is focused on the website
function isUserFocused() {
return document.hasFocus();
} }
function showNotification(title, body) { // Function to check if the user is in the same chat or group chat
if (Notification.permission === "granted") { function isUserInSameChat(sender, receiverId, groupId) {
new Notification(title, { body }); const currentChatUserId = document.getElementById('current-chat-user-id');
const currentGroupId = document.getElementById('current-group-id');
return (currentChatUserId && currentChatUserId.value == receiverId) || (currentGroupId && currentGroupId.value == groupId);
}
socket.on('new_message', (data) => {
const { sender, message, timestamp, receiver_id } = data;
if (!isUserInSameChat(sender, receiver_id, null) || !isUserFocused()) {
// Display notification logic here
console.log(`New message from ${sender}: ${message}`);
} }
} });
requestNotificationPermission(); socket.on('new_group_message', (data) => {
const { sender, message, timestamp, group_id } = data;
function handleMessageNotification(data) { if (!isUserInSameChat(sender, null, group_id) || !isUserFocused()) {
const title = `New Message from ${data.sender}`; // Display notification logic here
const body = data.content.length > 100 ? data.content.substring(0, 97) + '...' : data.content; console.log(`New group message from ${sender}: ${message}`);
showNotification(title, body); }
} });
window.notificationHandler = {
handleMessageNotification
};
}); });

View file

@ -0,0 +1,44 @@
// upload_handler.js
function handleFileInputChange(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);
}
}
function handleRemoveFile() {
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 = '';
}

View file

@ -640,6 +640,10 @@ video {
display: table; display: table;
} }
.hidden {
display: none;
}
.\!h-\[400px\] { .\!h-\[400px\] {
height: 400px !important; height: 400px !important;
} }

View file

@ -1,43 +1,46 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> <script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='output.css') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script src="{{ url_for('static', filename='js/notification.js') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='output.css') }}">
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script> <script src="{{ url_for('static', filename='js/notification.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script> <script src="{{ url_for('static', filename='js/common.js') }}"></script>
<script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script> <script src="{{ url_for('static', filename='js/upload_handler.js') }}"></script>
<script src="https://kit.fontawesome.com/47dbf3c43e.js" crossorigin="anonymous"></script> <script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
<script src="{{ url_for('static', filename='js/connection_check.js') }}"></script> <script src="https://kit.fontawesome.com/47dbf3c43e.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<input type="hidden" id="current-chat-user-id" value="{{ friend.id if friend else '' }}">
<input type="hidden" id="current-group-id" value="{{ group.id if group else '' }}">
<script> <script>
// Function to apply the theme based on user selection or system preference // Function to apply the theme based on user selection or system preference
function applyTheme(theme) { function applyTheme(theme) {
const root = document.documentElement; const root = document.documentElement;
if (theme === 'dark' || (theme === 'automatic' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (theme === 'dark' || (theme === 'automatic' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
root.classList.add('dark'); root.classList.add('dark');
} else { } else {
root.classList.remove('dark'); root.classList.remove('dark');
}
} }
}
// Apply the theme on page load based on the current theme setting // Apply the theme on page load based on the current theme setting
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
const currentTheme = localStorage.getItem('theme') || 'automatic'; const currentTheme = localStorage.getItem('theme') || 'automatic';
applyTheme(currentTheme); applyTheme(currentTheme);
// Listen for changes to the system theme and apply if the user selected 'automatic' // Listen for changes to the system theme and apply if the user selected 'automatic'
if (currentTheme === 'automatic') { if (currentTheme === 'automatic') {
window.matchMedia('(prefers-color-scheme: dark)').addListener(e => { window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
applyTheme('automatic'); applyTheme('automatic');
}); });
} }
}); });
</script> </script>
{% if 'username' in session %} {% if 'username' in session %}
<style> <style>
{{ current_user.custom_css | safe }} {{ current_user.custom_css | safe }}
</style> </style>
{% endif %} {% endif %}