conversation scripts and style separation

This commit is contained in:
2025-05-28 09:58:47 +02:00
parent 56d9b5e95b
commit 5c5d03e60c
5 changed files with 552 additions and 551 deletions

View File

@@ -0,0 +1,94 @@
.member-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
margin-right: 1rem;
}
.badge-creator {
background-color: #d1f2eb;
color: #117a65;
font-weight: 500;
}
.btn-remove-member {
background: #f8d7da;
color: #c82333;
border: none;
font-weight: 500;
}
.btn-remove-member:hover {
background: #f5c6cb;
color: #a71d2a;
}
/* Chat bubble styles */
.chat-messages {
padding: 1rem;
}
.message {
margin-bottom: 1.5rem;
}
.message-content {
position: relative;
padding: 0.75rem 1rem;
border-radius: 1rem;
max-width: 80%;
word-wrap: break-word;
}
.message.sent {
display: flex;
justify-content: flex-end;
}
.message.received {
display: flex;
justify-content: flex-start;
}
.message.sent .message-content {
background-color: var(--primary-color);
color: white;
border-top-right-radius: 0.25rem;
}
.message.received .message-content {
background-color: #f0f2f5;
color: #1c1e21;
border-top-left-radius: 0.25rem;
}
.message-info {
font-size: 0.75rem;
margin-bottom: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
}
.message.sent .message-info {
text-align: right;
background-color: rgba(0, 0, 0, 0.1);
color: rgba(255, 255, 255, 0.9);
}
.message.received .message-info {
background-color: rgba(0, 0, 0, 0.05);
color: #65676b;
}
.message.sent .message-info .text-muted {
color: rgba(255, 255, 255, 0.8) !important;
}
.message.received .message-info .text-muted {
color: #65676b !important;
}
.attachment {
margin-top: 0.5rem;
}
.message.sent .attachment .btn {
background-color: rgba(255, 255, 255, 0.2);
color: white;
border: none;
}
.message.sent .attachment .btn:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.message.received .attachment .btn {
background-color: #e4e6eb;
color: #1c1e21;
border: none;
}
.message.received .attachment .btn:hover {
background-color: #d8dadf;
}

151
static/js/chat-manager.js Normal file
View File

@@ -0,0 +1,151 @@
// Global state and socket management
if (typeof window.ChatManager === 'undefined') {
window.ChatManager = (function() {
let instance = null;
let socket = null;
const state = {
addedMessageIds: new Set(),
messageQueue: new Set(),
connectionState: {
hasJoined: false,
isConnected: false,
lastMessageId: null,
connectionAttempts: 0,
socketId: null
}
};
function init(conversationId) {
if (instance) {
return instance;
}
// Initialize message IDs from existing messages
$('.message').each(function() {
const messageId = $(this).data('message-id');
if (messageId) {
state.addedMessageIds.add(messageId);
}
});
// Create socket instance
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) {
console.error('Transport upgrade error:', err);
// Fall back to polling
socket.io.opts.transports = ['polling'];
});
instance = {
socket: socket,
state: state,
cleanup: cleanup
};
return instance;
}
function cleanup() {
console.log('Cleaning up socket connection');
if (socket) {
socket.off('new_message');
socket.disconnect();
socket = null;
}
instance = null;
}
return {
getInstance: function(conversationId) {
if (!instance) {
instance = init(conversationId);
}
return instance;
}
};
})();
}

223
static/js/conversation.js Normal file
View File

@@ -0,0 +1,223 @@
// Initialize chat when document is ready
$(document).ready(function() {
const conversationId = window.conversationId; // Set this in the template
const currentUserId = window.currentUserId; // Set this in the template
const chat = ChatManager.getInstance(conversationId);
const socket = chat.socket;
const state = chat.state;
console.log('Initializing chat for conversation:', conversationId);
// Join conversation room
socket.on('connect', function() {
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 appendMessage(message) {
const isCurrentUser = message.sender_id === currentUserId;
const messageHtml = `
<div class="message ${isCurrentUser ? 'sent' : 'received'}" data-message-id="${message.id}">
${!isCurrentUser ? `
<img src="${message.sender_avatar}"
alt="${message.sender_name}"
class="rounded-circle me-2"
style="width: 40px; height: 40px; object-fit: cover;">
` : ''}
<div class="message-content">
<div class="message-info">
<span class="fw-medium">${message.sender_name}</span>
<span class="text-muted ms-2">${message.created_at}</span>
</div>
${message.content}
${message.attachments && message.attachments.length > 0 ? `
<div class="attachments mt-2">
${message.attachments.map((attachment, index) => `
<div class="attachment mb-1">
<a href="${attachment.url}" class="btn btn-sm">
<i class="fas fa-paperclip me-1"></i>
${attachment.name}
<small class="ms-1">(${Math.round(attachment.size / 1024)} KB)</small>
</a>
</div>
`).join('')}
</div>
` : ''}
</div>
${isCurrentUser ? `
<img src="${message.sender_avatar}"
alt="${message.sender_name}"
class="rounded-circle ms-2"
style="width: 40px; height: 40px; object-fit: cover;">
` : ''}
</div>
`;
// Remove the "no messages" placeholder if it exists
$('.text-center.text-muted').remove();
$('#chatMessages').append(messageHtml);
scrollToBottom();
}
// Scroll to bottom of chat messages
function scrollToBottom() {
const chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
scrollToBottom();
// Message handling with deduplication and reconnection handling
socket.on('new_message', function(message) {
const timestamp = new Date().toISOString();
const messageKey = `${message.id}-${socket.id}`;
console.log('Message received:', {
id: message.id,
timestamp: timestamp,
socketId: socket.id,
messageKey: messageKey,
queueSize: state.messageQueue.size
});
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
$('#fileInput').on('change', function() {
const files = Array.from(this.files);
if (files.length > 0) {
const fileNames = files.map(file => file.name).join(', ');
$('#selectedFiles').text(files.length > 1 ? `${files.length} files selected: ${fileNames}` : fileNames);
} else {
$('#selectedFiles').text('');
}
});
// Handle message form submission with better error handling
let isSubmitting = false;
$('#messageForm').off('submit').on('submit', function(e) {
e.preventDefault();
e.stopPropagation();
if (isSubmitting) {
console.log('Message submission already in progress');
return false;
}
const messageInput = $('#messageInput');
const submitButton = $('#messageForm button[type="submit"]');
const submitIcon = submitButton.find('.fa-paper-plane');
const spinner = submitButton.find('.fa-circle-notch');
const message = messageInput.val().trim();
const fileInput = $('#fileInput')[0];
const files = Array.from(fileInput.files);
if (!message && files.length === 0) {
console.log('Empty message submission attempted');
return false;
}
console.log('Submitting message:', {
hasText: !!message,
fileCount: files.length,
socketId: socket.id,
connectionState: state.connectionState
});
isSubmitting = true;
messageInput.prop('disabled', true);
submitButton.prop('disabled', true);
submitIcon.addClass('d-none');
spinner.removeClass('d-none');
const formData = new FormData();
formData.append('message', message);
formData.append('csrf_token', $('input[name="csrf_token"]').val());
formData.append('socket_id', socket.id);
files.forEach((file, index) => {
formData.append(`file_${index}`, file);
});
formData.append('file_count', files.length);
$.ajax({
url: window.sendMessageUrl, // Set this in the template
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
console.log('Message sent successfully:', {
response: response,
socketId: socket.id
});
if (response.success) {
messageInput.val('');
fileInput.value = '';
$('#selectedFiles').text('');
// If socket is disconnected, append message directly
if (!state.connectionState.isConnected && response.message) {
console.log('Socket disconnected, appending message directly');
appendMessage(response.message);
}
} else {
console.error('Message send failed:', response);
alert('Failed to send message. Please try again.');
}
},
error: function(xhr, status, error) {
console.error('Failed to send message:', {
status: status,
error: error,
response: xhr.responseText
});
alert('Failed to send message. Please try again.');
},
complete: function() {
messageInput.prop('disabled', false);
submitButton.prop('disabled', false);
submitIcon.removeClass('d-none');
spinner.addClass('d-none');
isSubmitting = false;
}
});
return false;
});
// Clean up on page unload
$(window).on('beforeunload', function() {
chat.cleanup();
});
});

View File

@@ -0,0 +1,76 @@
$(document).ready(function() {
// Initialize Select2 for user selection
$('.select2').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Handle member removal
$(document).on('click', '.btn-remove-member', function() {
const memberRow = $(this).closest('.member-row');
memberRow.remove();
});
// Handle adding new member
$('#addMemberBtn').on('click', function() {
const select = $('#user_id');
const selectedOption = select.find('option:selected');
const selectedValue = select.val();
if (!selectedValue) {
return;
}
const userId = selectedValue;
const userName = selectedOption.text();
const userEmail = selectedOption.data('email');
const userAvatar = selectedOption.data('avatar');
// Check if user is already in the list
if ($(`.member-row[data-user-id="${userId}"]`).length > 0) {
return;
}
// Create new member row
const memberRow = `
<div class="list-group-item d-flex align-items-center member-row" data-user-id="${userId}">
<img class="member-avatar" src="${userAvatar}">
<div class="flex-grow-1">
<div class="fw-bold">${userName}</div>
<div class="text-muted small">${userEmail}</div>
</div>
<button type="button" class="btn btn-remove-member ms-2">
<i class="fas fa-user-minus me-1"></i>Remove
</button>
</div>
`;
// Add to the list
$('#selectedMembersList').append(memberRow);
// Clear the select and trigger change event
select.val(null).trigger('change');
});
// Handle form submission
$('#membersForm').on('submit', function(e) {
e.preventDefault();
// Collect all member IDs
const memberIds = [];
$('.member-row').each(function() {
memberIds.push($(this).data('user-id'));
});
// Remove any existing hidden inputs
$('input[name="members"]').remove();
// Add hidden input for each member ID
memberIds.forEach(function(id) {
$('#membersForm').append(`<input type="hidden" name="members" value="${id}">`);
});
// Submit the form
this.submit();
});
});

View File

@@ -6,102 +6,7 @@
{% block extra_css %} {% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<style> <link rel="stylesheet" href="{{ url_for('static', filename='css/conversation.css') }}">
.member-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
margin-right: 1rem;
}
.badge-creator {
background-color: #d1f2eb;
color: #117a65;
font-weight: 500;
}
.btn-remove-member {
background: #f8d7da;
color: #c82333;
border: none;
font-weight: 500;
}
.btn-remove-member:hover {
background: #f5c6cb;
color: #a71d2a;
}
/* Chat bubble styles */
.chat-messages {
padding: 1rem;
}
.message {
margin-bottom: 1.5rem;
}
.message-content {
position: relative;
padding: 0.75rem 1rem;
border-radius: 1rem;
max-width: 80%;
word-wrap: break-word;
}
.message.sent {
display: flex;
justify-content: flex-end;
}
.message.received {
display: flex;
justify-content: flex-start;
}
.message.sent .message-content {
background-color: var(--primary-color);
color: white;
border-top-right-radius: 0.25rem;
}
.message.received .message-content {
background-color: #f0f2f5;
color: #1c1e21;
border-top-left-radius: 0.25rem;
}
.message-info {
font-size: 0.75rem;
margin-bottom: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
}
.message.sent .message-info {
text-align: right;
background-color: rgba(0, 0, 0, 0.1);
color: rgba(255, 255, 255, 0.9);
}
.message.received .message-info {
background-color: rgba(0, 0, 0, 0.05);
color: #65676b;
}
.message.sent .message-info .text-muted {
color: rgba(255, 255, 255, 0.8) !important;
}
.message.received .message-info .text-muted {
color: #65676b !important;
}
.attachment {
margin-top: 0.5rem;
}
.message.sent .attachment .btn {
background-color: rgba(255, 255, 255, 0.2);
color: white;
border: none;
}
.message.sent .attachment .btn:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.message.received .attachment .btn {
background-color: #e4e6eb;
color: #1c1e21;
border: none;
}
.message.received .attachment .btn:hover {
background-color: #d8dadf;
}
</style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -290,461 +195,13 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script> <script>
// Initialize Select2 for user selection // Set global variables needed by the JavaScript files
$(document).ready(function() { window.conversationId = "{{ conversation.id }}";
$('.select2').select2({ window.currentUserId = "{{ current_user.id }}";
theme: 'bootstrap-5', window.sendMessageUrl = "{{ url_for('conversations.send_message', conversation_id=conversation.id) }}";
width: '100%'
});
// Handle member removal
$(document).on('click', '.btn-remove-member', function() {
const memberRow = $(this).closest('.member-row');
memberRow.remove();
});
// Handle adding new member
$('#addMemberBtn').on('click', function() {
const select = $('#user_id');
const selectedOption = select.find('option:selected');
const selectedValue = select.val();
if (!selectedValue) {
return;
}
const userId = selectedValue;
const userName = selectedOption.text();
const userEmail = selectedOption.data('email');
const userAvatar = selectedOption.data('avatar');
// Check if user is already in the list
if ($(`.member-row[data-user-id="${userId}"]`).length > 0) {
return;
}
// Create new member row
const memberRow = `
<div class="list-group-item d-flex align-items-center member-row" data-user-id="${userId}">
<img class="member-avatar" src="${userAvatar}">
<div class="flex-grow-1">
<div class="fw-bold">${userName}</div>
<div class="text-muted small">${userEmail}</div>
</div>
<button type="button" class="btn btn-remove-member ms-2">
<i class="fas fa-user-minus me-1"></i>Remove
</button>
</div>
`;
// Add to the list
$('#selectedMembersList').append(memberRow);
// Clear the select and trigger change event
select.val(null).trigger('change');
});
// Handle form submission
$('#membersForm').on('submit', function(e) {
e.preventDefault();
// Collect all member IDs
const memberIds = [];
$('.member-row').each(function() {
memberIds.push($(this).data('user-id'));
});
// Remove any existing hidden inputs
$('input[name="members"]').remove();
// Add hidden input for each member ID
memberIds.forEach(function(id) {
$('#membersForm').append(`<input type="hidden" name="members" value="${id}">`);
});
// Submit the form
this.submit();
});
});
// Global state and socket management
if (typeof window.ChatManager === 'undefined') {
window.ChatManager = (function() {
let instance = null;
let socket = null;
const state = {
addedMessageIds: new Set(),
messageQueue: new Set(),
connectionState: {
hasJoined: false,
isConnected: false,
lastMessageId: null,
connectionAttempts: 0,
socketId: null
}
};
// Store conversation ID globally
const conversationId = "{{ conversation.id }}";
function init() {
if (instance) {
return instance;
}
// Initialize message IDs from existing messages
$('.message').each(function() {
const messageId = $(this).data('message-id');
if (messageId) {
state.addedMessageIds.add(messageId);
}
});
// Create socket instance
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) {
console.error('Transport upgrade error:', err);
// Fall back to polling
socket.io.opts.transports = ['polling'];
});
instance = {
socket: socket,
state: state,
cleanup: cleanup
};
return instance;
}
function cleanup() {
console.log('Cleaning up socket connection');
if (socket) {
socket.off('new_message');
socket.disconnect();
socket = null;
}
instance = null;
}
return {
getInstance: function() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
}
// Initialize chat when document is ready
$(document).ready(function() {
const chat = ChatManager.getInstance();
const socket = chat.socket;
const state = chat.state;
const conversationId = "{{ conversation.id }}";
console.log('Initializing chat for conversation:', conversationId);
// Join conversation room
socket.on('connect', function() {
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 appendMessage(message) {
const currentUserId = "{{ current_user.id }}";
const isCurrentUser = message.sender_id === currentUserId;
const messageHtml = `
<div class="message ${isCurrentUser ? 'sent' : 'received'}" data-message-id="${message.id}">
${!isCurrentUser ? `
<img src="${message.sender_avatar}"
alt="${message.sender_name}"
class="rounded-circle me-2"
style="width: 40px; height: 40px; object-fit: cover;">
` : ''}
<div class="message-content">
<div class="message-info">
<span class="fw-medium">${message.sender_name}</span>
<span class="text-muted ms-2">${message.created_at}</span>
</div>
${message.content}
${message.attachments && message.attachments.length > 0 ? `
<div class="attachments mt-2">
${message.attachments.map((attachment, index) => `
<div class="attachment mb-1">
<a href="${attachment.url}" class="btn btn-sm">
<i class="fas fa-paperclip me-1"></i>
${attachment.name}
<small class="ms-1">(${Math.round(attachment.size / 1024)} KB)</small>
</a>
</div>
`).join('')}
</div>
` : ''}
</div>
${isCurrentUser ? `
<img src="${message.sender_avatar}"
alt="${message.sender_name}"
class="rounded-circle ms-2"
style="width: 40px; height: 40px; object-fit: cover;">
` : ''}
</div>
`;
// Remove the "no messages" placeholder if it exists
$('.text-center.text-muted').remove();
$('#chatMessages').append(messageHtml);
scrollToBottom();
}
// Scroll to bottom of chat messages
function scrollToBottom() {
const chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
scrollToBottom();
// Message handling with deduplication and reconnection handling
socket.on('new_message', function(message) {
const timestamp = new Date().toISOString();
const messageKey = `${message.id}-${socket.id}`;
console.log('Message received:', {
id: message.id,
timestamp: timestamp,
socketId: socket.id,
messageKey: messageKey,
queueSize: state.messageQueue.size
});
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
$('#fileInput').on('change', function() {
const files = Array.from(this.files);
if (files.length > 0) {
const fileNames = files.map(file => file.name).join(', ');
$('#selectedFiles').text(files.length > 1 ? `${files.length} files selected: ${fileNames}` : fileNames);
} else {
$('#selectedFiles').text('');
}
});
// Handle message form submission with better error handling
let isSubmitting = false;
$('#messageForm').off('submit').on('submit', function(e) {
e.preventDefault();
e.stopPropagation();
if (isSubmitting) {
console.log('Message submission already in progress');
return false;
}
const messageInput = $('#messageInput');
const submitButton = $('#messageForm button[type="submit"]');
const submitIcon = submitButton.find('.fa-paper-plane');
const spinner = submitButton.find('.fa-circle-notch');
const message = messageInput.val().trim();
const fileInput = $('#fileInput')[0];
const files = Array.from(fileInput.files);
if (!message && files.length === 0) {
console.log('Empty message submission attempted');
return false;
}
console.log('Submitting message:', {
hasText: !!message,
fileCount: files.length,
socketId: socket.id,
connectionState: state.connectionState
});
isSubmitting = true;
messageInput.prop('disabled', true);
submitButton.prop('disabled', true);
submitIcon.addClass('d-none');
spinner.removeClass('d-none');
const formData = new FormData();
formData.append('message', message);
formData.append('csrf_token', $('input[name="csrf_token"]').val());
formData.append('socket_id', socket.id);
files.forEach((file, index) => {
formData.append(`file_${index}`, file);
});
formData.append('file_count', files.length);
$.ajax({
url: "{{ url_for('conversations.send_message', conversation_id=conversation.id) }}",
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
console.log('Message sent successfully:', {
response: response,
socketId: socket.id
});
if (response.success) {
messageInput.val('');
fileInput.value = '';
$('#selectedFiles').text('');
// If socket is disconnected, append message directly
if (!state.connectionState.isConnected && response.message) {
console.log('Socket disconnected, appending message directly');
appendMessage(response.message);
}
} else {
console.error('Message send failed:', response);
alert('Failed to send message. Please try again.');
}
},
error: function(xhr, status, error) {
console.error('Failed to send message:', {
status: status,
error: error,
response: xhr.responseText
});
alert('Failed to send message. Please try again.');
},
complete: function() {
messageInput.prop('disabled', false);
submitButton.prop('disabled', false);
submitIcon.removeClass('d-none');
spinner.addClass('d-none');
isSubmitting = false;
}
});
return false;
});
// Clean up on page unload
$(window).on('beforeunload', function() {
chat.cleanup();
});
});
</script> </script>
<script src="{{ url_for('static', filename='js/chat-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/conversation.js') }}"></script>
<script src="{{ url_for('static', filename='js/member-management.js') }}"></script>
{% endblock %} {% endblock %}
{% endblock content %} {% endblock content %}