fixed messaging!
This commit is contained in:
Binary file not shown.
Binary file not shown.
5
app.py
5
app.py
@@ -11,7 +11,7 @@ from routes.trash import trash_bp
|
|||||||
from tasks import cleanup_trash
|
from tasks import cleanup_trash
|
||||||
import click
|
import click
|
||||||
from utils import timeago
|
from utils import timeago
|
||||||
from extensions import db, login_manager, csrf, socketio
|
from extensions import db, login_manager, csrf
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -31,7 +31,6 @@ def create_app():
|
|||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
login_manager.login_view = 'auth.login'
|
login_manager.login_view = 'auth.login'
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
socketio.init_app(app)
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_csrf_token():
|
def inject_csrf_token():
|
||||||
@@ -89,4 +88,4 @@ def profile_pic(filename):
|
|||||||
return send_from_directory(os.path.join(os.getcwd(), 'uploads', 'profile_pics'), filename)
|
return send_from_directory(os.path.join(os.getcwd(), 'uploads', 'profile_pics'), filename)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
socketio.run(app, debug=True)
|
app.run(debug=True)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from flask_socketio import SocketIO
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_wtf.csrf import CSRFProtect
|
from flask_wtf.csrf import CSRFProtect
|
||||||
@@ -6,5 +5,4 @@ from flask_wtf.csrf import CSRFProtect
|
|||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
socketio = SocketIO(cors_allowed_origins="*")
|
|
||||||
@@ -9,5 +9,4 @@ WTForms==3.1.1
|
|||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
Flask-SocketIO==5.3.6
|
|
||||||
email_validator==2.1.0.post1
|
email_validator==2.1.0.post1
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,13 +1,11 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_socketio import emit, join_room, leave_room
|
|
||||||
from models import db, Conversation, User, Message, MessageAttachment
|
from models import db, Conversation, User, Message, MessageAttachment
|
||||||
from forms import ConversationForm
|
from forms import ConversationForm
|
||||||
from routes.auth import require_password_change
|
from routes.auth import require_password_change
|
||||||
import os
|
import os
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from extensions import socketio
|
|
||||||
|
|
||||||
conversations_bp = Blueprint('conversations', __name__, url_prefix='/conversations')
|
conversations_bp = Blueprint('conversations', __name__, url_prefix='/conversations')
|
||||||
|
|
||||||
@@ -239,30 +237,45 @@ def delete_conversation(conversation_id):
|
|||||||
flash('Conversation has been deleted successfully.', 'success')
|
flash('Conversation has been deleted successfully.', 'success')
|
||||||
return redirect(url_for('conversations.conversations'))
|
return redirect(url_for('conversations.conversations'))
|
||||||
|
|
||||||
@socketio.on('join_conversation')
|
@conversations_bp.route('/<int:conversation_id>/messages', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def on_join(data):
|
@require_password_change
|
||||||
conversation_id = data.get('conversation_id')
|
def get_messages(conversation_id):
|
||||||
conversation = Conversation.query.get_or_404(conversation_id)
|
conversation = Conversation.query.get_or_404(conversation_id)
|
||||||
|
|
||||||
# Check if user is a member
|
# Check if user is a member
|
||||||
if not current_user.is_admin and current_user not in conversation.members:
|
if not current_user.is_admin and current_user not in conversation.members:
|
||||||
return
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
# Join the room
|
# Get the last message ID from the request
|
||||||
join_room(f'conversation_{conversation_id}')
|
last_message_id = request.args.get('last_message_id', type=int)
|
||||||
|
|
||||||
@socketio.on('leave_conversation')
|
# Query for new messages
|
||||||
@login_required
|
query = Message.query.filter_by(conversation_id=conversation_id)
|
||||||
def on_leave(data):
|
if last_message_id:
|
||||||
conversation_id = data.get('conversation_id')
|
query = query.filter(Message.id > last_message_id)
|
||||||
leave_room(f'conversation_{conversation_id}')
|
|
||||||
|
messages = query.order_by(Message.created_at.asc()).all()
|
||||||
@socketio.on('heartbeat')
|
|
||||||
@login_required
|
# Format messages for response
|
||||||
def on_heartbeat(data):
|
message_data = [{
|
||||||
# Just acknowledge the heartbeat to keep the connection alive
|
'id': message.id,
|
||||||
return {'status': 'ok'}
|
'content': message.content,
|
||||||
|
'created_at': message.created_at.strftime('%b %d, %Y %H:%M'),
|
||||||
|
'sender_id': str(message.user_id),
|
||||||
|
'sender_name': f"{message.user.username} {message.user.last_name}",
|
||||||
|
'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'),
|
||||||
|
'attachments': [{
|
||||||
|
'name': attachment.name,
|
||||||
|
'size': attachment.size,
|
||||||
|
'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index)
|
||||||
|
} for index, attachment in enumerate(message.attachments)]
|
||||||
|
} for message in messages]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'messages': message_data
|
||||||
|
})
|
||||||
|
|
||||||
@conversations_bp.route('/<int:conversation_id>/send_message', methods=['POST'])
|
@conversations_bp.route('/<int:conversation_id>/send_message', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -272,59 +285,46 @@ def send_message(conversation_id):
|
|||||||
|
|
||||||
# Check if user is a member
|
# Check if user is a member
|
||||||
if not current_user.is_admin and current_user not in conversation.members:
|
if not current_user.is_admin and current_user not in conversation.members:
|
||||||
return jsonify({'success': False, 'error': 'You do not have access to this conversation.'}), 403
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
message_content = request.form.get('message', '').strip()
|
message_content = request.form.get('message', '').strip()
|
||||||
file_count = int(request.form.get('file_count', 0))
|
file_count = int(request.form.get('file_count', 0))
|
||||||
|
|
||||||
if not message_content and file_count == 0:
|
if not message_content and file_count == 0:
|
||||||
return jsonify({'success': False, 'error': 'Message or file is required.'}), 400
|
return jsonify({'error': 'Message cannot be empty'}), 400
|
||||||
|
|
||||||
# Create new message
|
# Create the message
|
||||||
message = Message(
|
message = Message(
|
||||||
content=message_content,
|
content=message_content,
|
||||||
conversation_id=conversation_id,
|
user_id=current_user.id,
|
||||||
user_id=current_user.id
|
conversation_id=conversation_id
|
||||||
)
|
)
|
||||||
|
db.session.add(message)
|
||||||
# Create conversation-specific directory
|
db.session.flush() # Get the message ID
|
||||||
conversation_dir = os.path.join(UPLOAD_FOLDER, str(conversation_id))
|
|
||||||
os.makedirs(conversation_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Handle file attachments
|
# Handle file attachments
|
||||||
attachments = []
|
attachments = []
|
||||||
for i in range(file_count):
|
for i in range(file_count):
|
||||||
file = request.files.get(f'file_{i}')
|
file_key = f'file_{i}'
|
||||||
if file and file.filename:
|
if file_key in request.files:
|
||||||
if not allowed_file(file.filename):
|
file = request.files[file_key]
|
||||||
return jsonify({'success': False, 'error': f'File type not allowed: {file.filename}'}), 400
|
if file and allowed_file(file.filename):
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
if file.content_length and file.content_length > MAX_FILE_SIZE:
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
return jsonify({'success': False, 'error': f'File size exceeds limit: {file.filename}'}), 400
|
file.save(file_path)
|
||||||
|
|
||||||
# Generate unique filename
|
attachment = MessageAttachment(
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
message_id=message.id,
|
||||||
filename = secure_filename(file.filename)
|
name=filename,
|
||||||
unique_filename = f"{timestamp}_{filename}"
|
path=file_path,
|
||||||
file_path = os.path.join(conversation_dir, unique_filename)
|
size=os.path.getsize(file_path)
|
||||||
|
)
|
||||||
# Save file
|
db.session.add(attachment)
|
||||||
file.save(file_path)
|
attachments.append(attachment)
|
||||||
|
|
||||||
# Create attachment record
|
|
||||||
attachment = MessageAttachment(
|
|
||||||
name=filename,
|
|
||||||
path=file_path,
|
|
||||||
type=get_file_extension(filename),
|
|
||||||
size=os.path.getsize(file_path)
|
|
||||||
)
|
|
||||||
message.attachments.append(attachment)
|
|
||||||
attachments.append(attachment)
|
|
||||||
|
|
||||||
db.session.add(message)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Prepare message data for WebSocket
|
# Prepare message data for response
|
||||||
message_data = {
|
message_data = {
|
||||||
'id': message.id,
|
'id': message.id,
|
||||||
'content': message.content,
|
'content': message.content,
|
||||||
@@ -339,10 +339,6 @@ def send_message(conversation_id):
|
|||||||
} for index, attachment in enumerate(attachments)]
|
} for index, attachment in enumerate(attachments)]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Emit the message to all users in the conversation room
|
|
||||||
socketio.emit('new_message', message_data, room=f'conversation_{conversation_id}')
|
|
||||||
|
|
||||||
# Return response with message data
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': message_data
|
'message': message_data
|
||||||
@@ -369,43 +365,4 @@ def download_attachment(message_id, attachment_index):
|
|||||||
)
|
)
|
||||||
except (IndexError, Exception) as e:
|
except (IndexError, Exception) as e:
|
||||||
flash('File not found.', 'error')
|
flash('File not found.', 'error')
|
||||||
return redirect(url_for('conversations.conversation', conversation_id=conversation.id))
|
return redirect(url_for('conversations.conversation', conversation_id=conversation.id))
|
||||||
|
|
||||||
@conversations_bp.route('/<int:conversation_id>/messages')
|
|
||||||
@login_required
|
|
||||||
@require_password_change
|
|
||||||
def get_messages(conversation_id):
|
|
||||||
conversation = Conversation.query.get_or_404(conversation_id)
|
|
||||||
|
|
||||||
# Check if user is a member
|
|
||||||
if not current_user.is_admin and current_user not in conversation.members:
|
|
||||||
return jsonify({'success': False, 'error': 'You do not have access to this conversation.'}), 403
|
|
||||||
|
|
||||||
# Get the last message ID from the request
|
|
||||||
last_message_id = request.args.get('last_message_id', type=int)
|
|
||||||
|
|
||||||
# Query for new messages
|
|
||||||
query = Message.query.filter_by(conversation_id=conversation_id)
|
|
||||||
if last_message_id:
|
|
||||||
query = query.filter(Message.id > last_message_id)
|
|
||||||
|
|
||||||
messages = query.order_by(Message.created_at.asc()).all()
|
|
||||||
|
|
||||||
# Format messages for response
|
|
||||||
formatted_messages = []
|
|
||||||
for message in messages:
|
|
||||||
formatted_messages.append({
|
|
||||||
'id': message.id,
|
|
||||||
'content': message.content,
|
|
||||||
'created_at': message.created_at.strftime('%b %d, %Y %H:%M'),
|
|
||||||
'sender_id': str(message.user.id),
|
|
||||||
'sender_name': f"{message.user.username} {message.user.last_name}",
|
|
||||||
'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'),
|
|
||||||
'attachments': [{
|
|
||||||
'name': attachment.name,
|
|
||||||
'size': attachment.size,
|
|
||||||
'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index)
|
|
||||||
} for index, attachment in enumerate(message.attachments)]
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'messages': formatted_messages})
|
|
||||||
@@ -1,127 +1,45 @@
|
|||||||
// Global state and socket management
|
// Global state and polling management
|
||||||
if (typeof window.ChatManager === 'undefined') {
|
if (typeof window.ChatManager === 'undefined') {
|
||||||
window.ChatManager = (function() {
|
window.ChatManager = (function() {
|
||||||
let instance = null;
|
let instance = null;
|
||||||
let socket = null;
|
let pollInterval = null;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
addedMessageIds: new Set(),
|
addedMessageIds: new Set(),
|
||||||
messageQueue: new Set(),
|
messageQueue: new Set(),
|
||||||
connectionState: {
|
connectionState: {
|
||||||
hasJoined: false,
|
hasJoined: false,
|
||||||
isConnected: false,
|
isConnected: true,
|
||||||
lastMessageId: null,
|
lastMessageId: null,
|
||||||
connectionAttempts: 0,
|
pollAttempts: 0
|
||||||
socketId: null
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(conversationId) {
|
function init(conversationId) {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
|
console.log('[ChatManager] Instance already exists, returning existing instance');
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[ChatManager] Initializing new instance for conversation:', conversationId);
|
||||||
|
|
||||||
// Initialize message IDs from existing messages
|
// Initialize message IDs from existing messages
|
||||||
$('.message').each(function() {
|
$('.message').each(function() {
|
||||||
const messageId = $(this).data('message-id');
|
const messageId = $(this).data('message-id');
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
state.addedMessageIds.add(messageId);
|
state.addedMessageIds.add(messageId);
|
||||||
}
|
state.connectionState.lastMessageId = Math.max(state.connectionState.lastMessageId || 0, messageId);
|
||||||
});
|
console.log('[ChatManager] Initialized with existing message:', {
|
||||||
|
messageId: messageId,
|
||||||
// Create socket instance
|
lastMessageId: state.connectionState.lastMessageId
|
||||||
socket = io(window.location.origin, {
|
|
||||||
path: '/socket.io/',
|
|
||||||
transports: ['polling', 'websocket'],
|
|
||||||
upgrade: true,
|
|
||||||
reconnection: true,
|
|
||||||
reconnectionAttempts: Infinity,
|
|
||||||
reconnectionDelay: 1000,
|
|
||||||
reconnectionDelayMax: 5000,
|
|
||||||
timeout: 20000,
|
|
||||||
autoConnect: true,
|
|
||||||
forceNew: true,
|
|
||||||
multiplex: false,
|
|
||||||
pingTimeout: 60000,
|
|
||||||
pingInterval: 25000,
|
|
||||||
upgradeTimeout: 10000,
|
|
||||||
rememberUpgrade: true,
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
extraHeaders: {
|
|
||||||
'X-Forwarded-Proto': 'https'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up socket event handlers
|
|
||||||
socket.on('connect', function() {
|
|
||||||
state.connectionState.isConnected = true;
|
|
||||||
state.connectionState.connectionAttempts++;
|
|
||||||
state.connectionState.socketId = socket.id;
|
|
||||||
console.log('Socket connected:', {
|
|
||||||
attempt: state.connectionState.connectionAttempts,
|
|
||||||
socketId: socket.id,
|
|
||||||
existingSocketId: state.connectionState.socketId,
|
|
||||||
transport: socket.io.engine.transport.name
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always rejoin the room on connect
|
|
||||||
console.log('Joining conversation room:', conversationId);
|
|
||||||
socket.emit('join_conversation', {
|
|
||||||
conversation_id: conversationId,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
socketId: socket.id
|
|
||||||
});
|
|
||||||
state.connectionState.hasJoined = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', function(reason) {
|
|
||||||
console.log('Disconnected from conversation:', {
|
|
||||||
reason: reason,
|
|
||||||
socketId: socket.id,
|
|
||||||
connectionState: state.connectionState,
|
|
||||||
transport: socket.io.engine?.transport?.name
|
|
||||||
});
|
|
||||||
state.connectionState.isConnected = false;
|
|
||||||
state.connectionState.socketId = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connect_error', function(error) {
|
|
||||||
console.error('Connection error:', error);
|
|
||||||
// Try to reconnect with polling if websocket fails
|
|
||||||
if (socket.io.engine?.transport?.name === 'websocket') {
|
|
||||||
console.log('WebSocket failed, falling back to polling');
|
|
||||||
socket.io.opts.transports = ['polling'];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', function(error) {
|
|
||||||
console.error('Socket error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add heartbeat to keep connection alive
|
|
||||||
setInterval(function() {
|
|
||||||
if (socket.connected) {
|
|
||||||
socket.emit('heartbeat', {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
socketId: socket.id,
|
|
||||||
transport: socket.io.engine.transport.name
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 15000);
|
|
||||||
|
|
||||||
// Handle transport upgrade
|
|
||||||
socket.io.engine.on('upgrade', function() {
|
|
||||||
console.log('Transport upgraded to:', socket.io.engine.transport.name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.io.engine.on('upgradeError', function(err) {
|
// Start polling for new messages
|
||||||
console.error('Transport upgrade error:', err);
|
startPolling(conversationId);
|
||||||
// Fall back to polling
|
|
||||||
socket.io.opts.transports = ['polling'];
|
|
||||||
});
|
|
||||||
|
|
||||||
instance = {
|
instance = {
|
||||||
socket: socket,
|
|
||||||
state: state,
|
state: state,
|
||||||
cleanup: cleanup
|
cleanup: cleanup
|
||||||
};
|
};
|
||||||
@@ -129,12 +47,97 @@ if (typeof window.ChatManager === 'undefined') {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startPolling(conversationId) {
|
||||||
|
console.log('[ChatManager] Starting polling for conversation:', conversationId);
|
||||||
|
|
||||||
|
// Clear any existing polling
|
||||||
|
if (pollInterval) {
|
||||||
|
console.log('[ChatManager] Clearing existing polling interval');
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll every 3 seconds
|
||||||
|
pollInterval = setInterval(() => {
|
||||||
|
console.log('[ChatManager] Polling interval triggered');
|
||||||
|
fetchNewMessages(conversationId);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
console.log('[ChatManager] Performing initial message fetch');
|
||||||
|
fetchNewMessages(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchNewMessages(conversationId) {
|
||||||
|
const url = `/conversations/${conversationId}/messages`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (state.connectionState.lastMessageId) {
|
||||||
|
params.append('last_message_id', state.connectionState.lastMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ChatManager] Fetching new messages:', {
|
||||||
|
url: url,
|
||||||
|
params: params.toString(),
|
||||||
|
lastMessageId: state.connectionState.lastMessageId
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`${url}?${params.toString()}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('[ChatManager] Received messages response:', {
|
||||||
|
success: data.success,
|
||||||
|
messageCount: data.messages ? data.messages.length : 0,
|
||||||
|
messages: data.messages
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.success && data.messages) {
|
||||||
|
state.connectionState.pollAttempts = 0;
|
||||||
|
processNewMessages(data.messages);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[ChatManager] Error fetching messages:', error);
|
||||||
|
state.connectionState.pollAttempts++;
|
||||||
|
|
||||||
|
// If we've had too many failed attempts, try to reconnect
|
||||||
|
if (state.connectionState.pollAttempts > 5) {
|
||||||
|
console.log('[ChatManager] Too many failed attempts, restarting polling');
|
||||||
|
startPolling(conversationId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processNewMessages(messages) {
|
||||||
|
console.log('[ChatManager] Processing new messages:', {
|
||||||
|
messageCount: messages.length,
|
||||||
|
currentLastMessageId: state.connectionState.lastMessageId,
|
||||||
|
existingMessageIds: Array.from(state.addedMessageIds)
|
||||||
|
});
|
||||||
|
|
||||||
|
messages.forEach(message => {
|
||||||
|
console.log('[ChatManager] Processing message:', {
|
||||||
|
messageId: message.id,
|
||||||
|
alreadyAdded: state.addedMessageIds.has(message.id),
|
||||||
|
currentLastMessageId: state.connectionState.lastMessageId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state.addedMessageIds.has(message.id)) {
|
||||||
|
state.addedMessageIds.add(message.id);
|
||||||
|
state.connectionState.lastMessageId = Math.max(state.connectionState.lastMessageId || 0, message.id);
|
||||||
|
console.log('[ChatManager] Triggering new_message event for message:', message.id);
|
||||||
|
// Trigger the new message event
|
||||||
|
$(document).trigger('new_message', [message]);
|
||||||
|
} else {
|
||||||
|
console.log('[ChatManager] Skipping already added message:', message.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
console.log('Cleaning up socket connection');
|
console.log('[ChatManager] Cleaning up polling');
|
||||||
if (socket) {
|
if (pollInterval) {
|
||||||
socket.off('new_message');
|
clearInterval(pollInterval);
|
||||||
socket.disconnect();
|
pollInterval = null;
|
||||||
socket = null;
|
|
||||||
}
|
}
|
||||||
instance = null;
|
instance = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ $(document).ready(function() {
|
|||||||
const conversationId = window.conversationId; // Set this in the template
|
const conversationId = window.conversationId; // Set this in the template
|
||||||
const currentUserId = window.currentUserId; // Set this in the template
|
const currentUserId = window.currentUserId; // Set this in the template
|
||||||
const chat = ChatManager.getInstance(conversationId);
|
const chat = ChatManager.getInstance(conversationId);
|
||||||
const socket = chat.socket;
|
|
||||||
const state = chat.state;
|
const state = chat.state;
|
||||||
|
|
||||||
console.log('Initializing chat for conversation:', conversationId);
|
console.log('[Conversation] Initializing chat for conversation:', conversationId);
|
||||||
|
|
||||||
// Join conversation room
|
// Keep track of messages we've already displayed
|
||||||
socket.on('connect', function() {
|
const displayedMessageIds = new Set();
|
||||||
if (!state.connectionState.hasJoined) {
|
|
||||||
console.log('Joining conversation room:', conversationId);
|
|
||||||
socket.emit('join_conversation', {
|
|
||||||
conversation_id: conversationId,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
socketId: socket.id
|
|
||||||
});
|
|
||||||
state.connectionState.hasJoined = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to append a new message to the chat
|
// Function to append a new message to the chat
|
||||||
function appendMessage(message) {
|
function appendMessage(message) {
|
||||||
|
console.log('[Conversation] Attempting to append message:', {
|
||||||
|
messageId: message.id,
|
||||||
|
content: message.content,
|
||||||
|
senderId: message.sender_id,
|
||||||
|
currentUserId: currentUserId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if we've already displayed this message
|
||||||
|
if (displayedMessageIds.has(message.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
displayedMessageIds.add(message.id);
|
||||||
|
|
||||||
const isCurrentUser = message.sender_id === currentUserId;
|
const isCurrentUser = message.sender_id === currentUserId;
|
||||||
const messageHtml = `
|
const messageHtml = `
|
||||||
<div class="message ${isCurrentUser ? 'sent' : 'received'}" data-message-id="${message.id}">
|
<div class="message ${isCurrentUser ? 'sent' : 'received'}" data-message-id="${message.id}">
|
||||||
@@ -65,9 +67,18 @@ $(document).ready(function() {
|
|||||||
$('.text-center.text-muted').remove();
|
$('.text-center.text-muted').remove();
|
||||||
|
|
||||||
$('#chatMessages').append(messageHtml);
|
$('#chatMessages').append(messageHtml);
|
||||||
|
console.log('[Conversation] Message appended to chat:', message.id);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize displayedMessageIds with existing messages
|
||||||
|
$('.message').each(function() {
|
||||||
|
const messageId = $(this).data('message-id');
|
||||||
|
if (messageId) {
|
||||||
|
displayedMessageIds.add(messageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Scroll to bottom of chat messages
|
// Scroll to bottom of chat messages
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
const chatMessages = document.getElementById('chatMessages');
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
@@ -75,41 +86,14 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
// Message handling with deduplication and reconnection handling
|
// Listen for new messages
|
||||||
socket.on('new_message', function(message) {
|
$(document).on('new_message', function(event, message) {
|
||||||
const timestamp = new Date().toISOString();
|
console.log('[Conversation] Received new_message event:', {
|
||||||
const messageKey = `${message.id}-${socket.id}`;
|
messageId: message.id,
|
||||||
|
eventType: event.type,
|
||||||
console.log('Message received:', {
|
timestamp: new Date().toISOString()
|
||||||
id: message.id,
|
|
||||||
timestamp: timestamp,
|
|
||||||
socketId: socket.id,
|
|
||||||
messageKey: messageKey,
|
|
||||||
queueSize: state.messageQueue.size
|
|
||||||
});
|
});
|
||||||
|
appendMessage(message);
|
||||||
if (state.messageQueue.has(messageKey)) {
|
|
||||||
console.log('Message already in queue:', messageKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.messageQueue.add(messageKey);
|
|
||||||
|
|
||||||
if (!state.addedMessageIds.has(message.id)) {
|
|
||||||
console.log('Processing new message:', message.id);
|
|
||||||
appendMessage(message);
|
|
||||||
state.connectionState.lastMessageId = message.id;
|
|
||||||
state.addedMessageIds.add(message.id);
|
|
||||||
} else {
|
|
||||||
console.log('Duplicate message detected:', {
|
|
||||||
messageId: message.id,
|
|
||||||
lastMessageId: state.connectionState.lastMessageId,
|
|
||||||
socketId: socket.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up message from queue after processing
|
|
||||||
state.messageQueue.delete(messageKey);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle file selection
|
// Handle file selection
|
||||||
@@ -123,14 +107,14 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle message form submission with better error handling
|
// Handle message form submission
|
||||||
let isSubmitting = false;
|
let isSubmitting = false;
|
||||||
$('#messageForm').off('submit').on('submit', function(e) {
|
$('#messageForm').off('submit').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isSubmitting) {
|
if (isSubmitting) {
|
||||||
console.log('Message submission already in progress');
|
console.log('[Conversation] Message submission already in progress');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,15 +127,14 @@ $(document).ready(function() {
|
|||||||
const files = Array.from(fileInput.files);
|
const files = Array.from(fileInput.files);
|
||||||
|
|
||||||
if (!message && files.length === 0) {
|
if (!message && files.length === 0) {
|
||||||
console.log('Empty message submission attempted');
|
console.log('[Conversation] Empty message submission attempted');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Submitting message:', {
|
console.log('[Conversation] Submitting message:', {
|
||||||
hasText: !!message,
|
hasText: !!message,
|
||||||
fileCount: files.length,
|
fileCount: files.length,
|
||||||
socketId: socket.id,
|
timestamp: new Date().toISOString()
|
||||||
connectionState: state.connectionState
|
|
||||||
});
|
});
|
||||||
|
|
||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
@@ -163,44 +146,46 @@ $(document).ready(function() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('message', message);
|
formData.append('message', message);
|
||||||
formData.append('csrf_token', $('input[name="csrf_token"]').val());
|
formData.append('csrf_token', $('input[name="csrf_token"]').val());
|
||||||
formData.append('socket_id', socket.id);
|
formData.append('file_count', files.length);
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
formData.append(`file_${index}`, file);
|
formData.append(`file_${index}`, file);
|
||||||
});
|
});
|
||||||
formData.append('file_count', files.length);
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.sendMessageUrl, // Set this in the template
|
url: window.sendMessageUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData,
|
data: formData,
|
||||||
processData: false,
|
processData: false,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
console.log('Message sent successfully:', {
|
console.log('[Conversation] Message sent successfully:', {
|
||||||
response: response,
|
response: response,
|
||||||
socketId: socket.id
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
messageInput.val('');
|
messageInput.val('');
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
$('#selectedFiles').text('');
|
$('#selectedFiles').text('');
|
||||||
|
|
||||||
// If socket is disconnected, append message directly
|
// Append the message directly since we sent it
|
||||||
if (!state.connectionState.isConnected && response.message) {
|
if (response.message) {
|
||||||
console.log('Socket disconnected, appending message directly');
|
console.log('[Conversation] Appending sent message directly:', response.message.id);
|
||||||
|
// Update the ChatManager's lastMessageId
|
||||||
|
chat.state.connectionState.lastMessageId = response.message.id;
|
||||||
appendMessage(response.message);
|
appendMessage(response.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Message send failed:', response);
|
console.error('[Conversation] Message send failed:', response);
|
||||||
alert('Failed to send message. Please try again.');
|
alert('Failed to send message. Please try again.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
console.error('Failed to send message:', {
|
console.error('[Conversation] Failed to send message:', {
|
||||||
status: status,
|
status: status,
|
||||||
error: error,
|
error: error,
|
||||||
response: xhr.responseText
|
response: xhr.responseText,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
alert('Failed to send message. Please try again.');
|
alert('Failed to send message. Please try again.');
|
||||||
},
|
},
|
||||||
@@ -210,6 +195,7 @@ $(document).ready(function() {
|
|||||||
submitIcon.removeClass('d-none');
|
submitIcon.removeClass('d-none');
|
||||||
spinner.addClass('d-none');
|
spinner.addClass('d-none');
|
||||||
isSubmitting = false;
|
isSubmitting = false;
|
||||||
|
console.log('[Conversation] Message submission completed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,6 +204,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Clean up on page unload
|
// Clean up on page unload
|
||||||
$(window).on('beforeunload', function() {
|
$(window).on('beforeunload', function() {
|
||||||
|
console.log('[Conversation] Cleaning up on page unload');
|
||||||
chat.cleanup();
|
chat.cleanup();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
99
templates/settings/tabs/logs.html
Normal file
99
templates/settings/tabs/logs.html
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{% macro logs_tab() %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="logFiltersForm" class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="logLevel" class="form-label">Log Level</label>
|
||||||
|
<select class="form-select" id="logLevel" name="level">
|
||||||
|
<option value="">All Levels</option>
|
||||||
|
<option value="INFO">Info</option>
|
||||||
|
<option value="WARNING">Warning</option>
|
||||||
|
<option value="ERROR">Error</option>
|
||||||
|
<option value="DEBUG">Debug</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="logCategory" class="form-label">Category</label>
|
||||||
|
<select class="form-select" id="logCategory" name="category">
|
||||||
|
<option value="">All Categories</option>
|
||||||
|
<option value="AUTH">Authentication</option>
|
||||||
|
<option value="FILE">File Operations</option>
|
||||||
|
<option value="USER">User Management</option>
|
||||||
|
<option value="SYSTEM">System</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="startDate" class="form-label">Start Date</label>
|
||||||
|
<input type="date" class="form-control" id="startDate" name="start_date">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="endDate" class="form-label">End Date</label>
|
||||||
|
<input type="date" class="form-control" id="endDate" name="end_date">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="searchLogs" class="form-label">Search</label>
|
||||||
|
<input type="text" class="form-control" id="searchLogs" name="search" placeholder="Search in logs...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary me-2">
|
||||||
|
<i class="fas fa-search me-1"></i> Apply Filters
|
||||||
|
</button>
|
||||||
|
<button type="reset" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-undo me-1"></i> Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs Table -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Level</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logsTableBody">
|
||||||
|
<!-- Logs will be loaded here via JavaScript -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<nav aria-label="Logs pagination" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center" id="logsPagination">
|
||||||
|
<!-- Pagination will be loaded here via JavaScript -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Details Modal -->
|
||||||
|
<div class="modal fade" id="logDetailsModal" tabindex="-1" aria-labelledby="logDetailsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="logDetailsModalLabel">Log Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="logDetailsContent">
|
||||||
|
<!-- Log details will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
Reference in New Issue
Block a user