From 5c5d03e60c5f6e9ceb3d1c36a92a2f6eec76b912 Mon Sep 17 00:00:00 2001 From: Kobe Date: Wed, 28 May 2025 09:58:47 +0200 Subject: [PATCH] conversation scripts and style separation --- static/css/conversation.css | 94 ++++ static/js/chat-manager.js | 151 ++++++ static/js/conversation.js | 223 +++++++++ static/js/member-management.js | 76 +++ templates/conversations/conversation.html | 559 +--------------------- 5 files changed, 552 insertions(+), 551 deletions(-) create mode 100644 static/css/conversation.css create mode 100644 static/js/chat-manager.js create mode 100644 static/js/conversation.js create mode 100644 static/js/member-management.js diff --git a/static/css/conversation.css b/static/css/conversation.css new file mode 100644 index 0000000..b0913e0 --- /dev/null +++ b/static/css/conversation.css @@ -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; +} \ No newline at end of file diff --git a/static/js/chat-manager.js b/static/js/chat-manager.js new file mode 100644 index 0000000..652b268 --- /dev/null +++ b/static/js/chat-manager.js @@ -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; + } + }; + })(); +} \ No newline at end of file diff --git a/static/js/conversation.js b/static/js/conversation.js new file mode 100644 index 0000000..36d2162 --- /dev/null +++ b/static/js/conversation.js @@ -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 = ` +
+ ${!isCurrentUser ? ` + ${message.sender_name} + ` : ''} +
+
+ ${message.sender_name} + ${message.created_at} +
+ ${message.content} + ${message.attachments && message.attachments.length > 0 ? ` +
+ ${message.attachments.map((attachment, index) => ` + + `).join('')} +
+ ` : ''} +
+ ${isCurrentUser ? ` + ${message.sender_name} + ` : ''} +
+ `; + + // 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(); + }); +}); \ No newline at end of file diff --git a/static/js/member-management.js b/static/js/member-management.js new file mode 100644 index 0000000..d7440bc --- /dev/null +++ b/static/js/member-management.js @@ -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 = ` +
+ +
+
${userName}
+
${userEmail}
+
+ +
+ `; + + // 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(``); + }); + + // Submit the form + this.submit(); + }); +}); \ No newline at end of file diff --git a/templates/conversations/conversation.html b/templates/conversations/conversation.html index 234f5c4..923f7e8 100644 --- a/templates/conversations/conversation.html +++ b/templates/conversations/conversation.html @@ -6,102 +6,7 @@ {% block extra_css %} - + {% endblock %} {% block content %} @@ -290,461 +195,13 @@ + + + {% endblock %} {% endblock content %} \ No newline at end of file