diff --git a/osinaweb/db.sqlite3 b/osinaweb/db.sqlite3 index ac7a6cb0..bf5a2032 100644 Binary files a/osinaweb/db.sqlite3 and b/osinaweb/db.sqlite3 differ diff --git a/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc b/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc index 5c256439..b471c4ba 100644 Binary files a/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc and b/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc differ diff --git a/osinaweb/osichat/__pycache__/models.cpython-310.pyc b/osinaweb/osichat/__pycache__/models.cpython-310.pyc index bc9650cf..38bca7e0 100644 Binary files a/osinaweb/osichat/__pycache__/models.cpython-310.pyc and b/osinaweb/osichat/__pycache__/models.cpython-310.pyc differ diff --git a/osinaweb/osichat/consumers.py b/osinaweb/osichat/consumers.py index 5906a68c..439f2172 100644 --- a/osinaweb/osichat/consumers.py +++ b/osinaweb/osichat/consumers.py @@ -60,13 +60,19 @@ class OsitcomVisitor(WebsocketConsumer): 'visitors': visitors, } if self.client_type == 'mobile_admin': - visitor_data = [] + visitors_data = [] for visitor in visitors: - visitor_data = model_to_dict(visitor) - + visitor_dict = model_to_dict(visitor) + visitor_logs = VisitorLog.objects.filter(visitor=visitor) + + visitor_logs_data = [model_to_dict(log) for log in visitor_logs] + visitor_dict['visitor_logs'] = visitor_logs_data + + visitors_data.append(visitor_dict) + self.send(text_data=json.dumps({ 'event_type': 'get_visitors', - 'visitor_data': visitor_data, + 'visitors_data': visitors_data, }, cls=DjangoJSONEncoder)) @@ -147,6 +153,7 @@ class OsitcomChatRooms(WebsocketConsumer): chat_room = get_object_or_404(ChatRoom, id=event['chatroom_id']) user = get_object_or_404(User, id=self.user_id) number_of_unread = ChatMessage.objects.filter(room=chat_room).exclude(member=user).exclude(chatmessageseen__member=user).count() + last_message = ChatMessage.objects.filter(room=chat_room).last() context = { 'chat_room': chat_room, @@ -163,6 +170,7 @@ class OsitcomChatRooms(WebsocketConsumer): self.send(text_data=json.dumps({ 'event_type': 'new_update', 'chatroom_id': chat_room.id, + 'user': last_message.member.id if last_message and last_message.member else None, 'html': html, })) @@ -237,10 +245,6 @@ class OsitcomChatRoom(WebsocketConsumer): ) self.chat_room = chat_room self.group = f"{self.session_id}_{self.chat_room.id}" - event = { - 'type': 'start_conversation_handler', - 'chat_room_id': chat_room.id - } async_to_sync(self.channel_layer.group_discard)( self.group, self.channel_name ) @@ -251,6 +255,11 @@ class OsitcomChatRoom(WebsocketConsumer): self.group, event ) + event = { + 'type': 'start_conversation_handler', + 'chat_room_id': chat_room.id + } + if event_type == 'typing': event = { 'type': 'typing_handler', diff --git a/osinaweb/osinacore/templates/chat_templates/chat-room.html b/osinaweb/osinacore/templates/chat_templates/chat-room.html index 571ac787..6ee49f84 100644 --- a/osinaweb/osinacore/templates/chat_templates/chat-room.html +++ b/osinaweb/osinacore/templates/chat_templates/chat-room.html @@ -1,5 +1,19 @@ {% load static %} -
+ + + + +
@@ -9,113 +23,113 @@

{{chat_room.chatroomguest.visitor.name}}

- -
- {% for message in chat_room_messages %} - {% if message.member %} - {% if not message.chatmessageattachment %} -
-
-

{{message.content}}

-
-
- {% else %} - {% if message.chatmessageattachment.is_image %} +
+ +
+ {% for message in chat_room_messages %} + {% if message.member %} + {% if not message.chatmessageattachment %}
-
- +
+

{{message.content}}

-
+
{% else %} -
-
-
-
- - - - - -
-
- {{message.chatmessageattachment.file_name}} + {% if message.chatmessageattachment.is_image %} +
+
+ +
+
+ {% else %} +
+
+
+
+ + + + + +
+
+ {{message.chatmessageattachment.file_name}} +
-
+ {% endif %} {% endif %} - {% endif %} - {% else %} -
-
-
- -
-
- {% if not message.chatmessageattachment %} -
-

{{message.content}}

+ {% else %} +
+
+
+ +
- {% else %} - {% if message.chatmessageattachment.is_image %} + {% if not message.chatmessageattachment %}
- + class="max-w-[80%] bg-gray-50 px-4 py-3 rounded-r-3xl rounded-tl-3xl text-secondosiblue text-sm leading-6 bg-opacity-50 shadow-md border border-gray-100"> +

{{message.content}}

{% else %} -
-
-
- - - - - -
-
- {{message.chatmessageattachment.file_name}} + {% if message.chatmessageattachment.is_image %} +
+ +
+ {% else %} +
+
+
+ + + + + +
+
+ {{message.chatmessageattachment.file_name}} +
-
+ {% endif %} {% endif %} - {% endif %} -
- {% endif %} - {% endfor %} - - +
+ {% endif %} + {% endfor %} - -
+ +
- -
- {% csrf_token %} -
- -
-
- - - - - + + + {% csrf_token %} +
+ +
+
+ + + + + +
+
-
-
- + +
\ No newline at end of file diff --git a/osinaweb/osinacore/templates/chat_templates/chat-widget.html b/osinaweb/osinacore/templates/chat_templates/chat-widget.html index 300c427e..838d79cd 100644 --- a/osinaweb/osinacore/templates/chat_templates/chat-widget.html +++ b/osinaweb/osinacore/templates/chat_templates/chat-widget.html @@ -4,11 +4,11 @@
-
+
-
+
diff --git a/osinaweb/osinacore/templates/chat_templates/partials/new-chat-room.html b/osinaweb/osinacore/templates/chat_templates/partials/new-chat-room.html index 1b524176..63855b16 100644 --- a/osinaweb/osinacore/templates/chat_templates/partials/new-chat-room.html +++ b/osinaweb/osinacore/templates/chat_templates/partials/new-chat-room.html @@ -2,8 +2,8 @@

{{chat_room.chatroomguest.visitor.ip_address}}

-
+
diff --git a/osinaweb/osinacore/templates/chat_templates/partials/rooms.html b/osinaweb/osinacore/templates/chat_templates/partials/rooms.html index 9be254ca..f48e1fd4 100644 --- a/osinaweb/osinacore/templates/chat_templates/partials/rooms.html +++ b/osinaweb/osinacore/templates/chat_templates/partials/rooms.html @@ -1,49 +1,65 @@ - -
-
- - - -

Chats

-
- - -
- - - - - -

Visitors

-
+ + -
- - {% for chat_room in chat_rooms %} -
-
-

{{chat_room.chatroomguest.visitor.ip_address}}

- -
-

{{chat_room.number_of_unread}}

-
-
- -

{{chat_room.chatmessage_set.all.last.content}}

- -

{{chat_room.last_updated}}

-
- {% endfor %} -
\ No newline at end of file + +
+
+ + + +

Chats

+
+ + +
+ + + + + +

Visitors

+
+
+ +
+ + {% for chat_room in chat_rooms %} +
+
+

{{chat_room.chatroomguest.visitor.ip_address}}

+ + +
+ +

{{chat_room.chatmessage_set.all.last.content}}

+ +

{{chat_room.last_updated}}

+
+ {% endfor %} +
+ + \ No newline at end of file diff --git a/osinaweb/static/dist/output.css b/osinaweb/static/dist/output.css index 19e48c24..84dfc6cb 100644 --- a/osinaweb/static/dist/output.css +++ b/osinaweb/static/dist/output.css @@ -714,6 +714,10 @@ video { bottom: 4rem; } +.bottom-2 { + bottom: 0.5rem; +} + .bottom-20 { bottom: 5rem; } @@ -722,10 +726,18 @@ video { bottom: 6rem; } +.bottom-28 { + bottom: 7rem; +} + .bottom-3 { bottom: 0.75rem; } +.bottom-32 { + bottom: 8rem; +} + .bottom-5 { bottom: 1.25rem; } @@ -842,18 +854,6 @@ video { top: 14px; } -.bottom-2 { - bottom: 0.5rem; -} - -.bottom-28 { - bottom: 7rem; -} - -.bottom-32 { - bottom: 8rem; -} - .z-10 { z-index: 10; } @@ -1103,6 +1103,10 @@ video { height: 210px; } +.h-\[22px\] { + height: 22px; +} + .h-\[250px\] { height: 250px; } @@ -1200,10 +1204,6 @@ video { height: 100vh; } -.h-\[22px\] { - height: 22px; -} - .max-h-\[200px\] { max-height: 200px; } @@ -1332,6 +1332,10 @@ video { width: 220px; } +.w-\[22px\] { + width: 22px; +} + .w-\[240px\] { width: 240px; } @@ -1449,10 +1453,6 @@ video { width: 100%; } -.w-\[22px\] { - width: 22px; -} - .min-w-full { min-width: 100%; } @@ -1843,6 +1843,11 @@ video { border-top-right-radius: 0px; } +.rounded-l { + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + .rounded-bl-md { border-bottom-left-radius: 0.375rem; } @@ -2196,6 +2201,10 @@ video { --tw-bg-opacity: 0.8; } +.bg-opacity-60 { + --tw-bg-opacity: 0.6; +} + .bg-gradient-to-b { background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); } @@ -3190,6 +3199,10 @@ video { } } */ +.hover\:pointer-events-none:hover { + pointer-events: none; +} + .hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; @@ -3202,6 +3215,10 @@ video { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.hover\:overflow-hidden:hover { + overflow: hidden; +} + .hover\:bg-gray-100:hover { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity)); diff --git a/osinaweb/static/js/osichat-admin/inner-conversation.js b/osinaweb/static/js/osichat-admin/inner-conversation.js index 096611d1..6db0a1e2 100644 --- a/osinaweb/static/js/osichat-admin/inner-conversation.js +++ b/osinaweb/static/js/osichat-admin/inner-conversation.js @@ -2,99 +2,154 @@ function handleChatRoomClick(event) { const sessionId = event.currentTarget.getAttribute('data-session'); const chatId = event.currentTarget.getAttribute('data-roomid'); - if (sessionId && chatId) { + if (sessionId && chatId && chatId !== currentChatId) { + showLoader(); openConversation(sessionId, chatId); - } else { - console.error('Session ID not found for this chat room.'); + currentChatId = chatId; } } - document.querySelectorAll('.chat-room').forEach(div => { div.addEventListener('click', handleChatRoomClick); }); - function appendTextAreaScript(domain, conversationContainer) { - if (!document.querySelector(`script[src="${protocol}://${admin_chat_domain}/static/js/osichat-admin/textarea.js"]`)) { - const textareaScript = document.createElement('script'); - textareaScript.type = 'text/javascript'; - textareaScript.src = `${protocol}://${admin_chat_domain}/static/js/osichat-admin/textarea.js`; - conversationContainer.appendChild(textareaScript); + + function markCurrentChatRead(chatid) { + const unreadElement = document.querySelector(`.unread[data-roomid='${chatid}']`); + if (unreadElement) { + unreadElement.classList.add('hidden'); } } + // FUNCTIONS TO SHOW & HIDE THE LOADER + function showLoader() { + const roomLoader = document.getElementById('roomLoader'); + if (roomLoader) { + roomLoader.classList.remove('hidden'); + } - function openConversation(sessionid, chatid) { - if (osichatadminroomSocket && osichatadminroomSocket.readyState !== WebSocket.CLOSED) { //Close previous sockets - osichatadminroomSocket.close(); - } - osichatadminroomSocket = new WebSocket(`${admin_chat_ws_scheme}://${admin_chat_domain}/ws/osichat-admin/${sessionid}/${chatid}/`); - osichatadminroomSocket.onopen = function () { - console.log('WebSocket connection to osichat established'); - osichatadminroomSocket.send(JSON.stringify({ 'event_type': 'load_chat', 'client_type': 'website_admin' })); - osichatadminroomSocket.send(JSON.stringify({ 'event_type': 'update_read_messages', 'user_id': userId, 'chat_state': 'open' })); - }; - function handleLoadChatEvent(data, osichatadminroomSocket) { - let chatDiv = document.getElementById('widgetRightSide'); - chatDiv.innerHTML = data.html; - appendTextAreaScript(admin_chat_domain, chatDiv); - - const sendMessageForm = document.querySelector('#sendMessage'); - sendMessageForm.addEventListener('submit', function (event) { - event.preventDefault(); - const message = event.target.elements.message.value; - const eventMessage = { - 'event_type': 'send_message', - 'message': message, - 'user_id': userId - }; - - osichatadminroomSocket.send(JSON.stringify(eventMessage)); - event.target.reset(); - }); - } + const widgetLeftSide = document.getElementById('widgetLeftSide'); + widgetLeftSide.classList.remove('overflow-y-auto'); + widgetLeftSide.classList.add('overflow-hidden'); + } - osichatadminroomSocket.onmessage = function (e) { - const data = JSON.parse(e.data); - const typingDiv = document.getElementById('typing'); - const messagesDiv = document.getElementById('messages_container'); - switch (data.event_type) { - case 'load_chat': - handleLoadChatEvent(data, osichatadminroomSocket); - break; - case 'typing': - if(!typingDiv && data.user != userId){ - messagesDiv.insertAdjacentHTML('beforeend', data.html); - } - break; - case 'stopped_typing': + function hideLoader() { + const roomLoader = document.getElementById('roomLoader'); + if (roomLoader) { + roomLoader.classList.add('hidden'); + } + + const widgetLeftSide = document.getElementById('widgetLeftSide'); + widgetLeftSide.classList.remove('overflow-hidden'); + widgetLeftSide.classList.add('overflow-y-auto'); + } + + function appendTextAreaScript(conversationContainer) { + const textareaScript = document.createElement('script'); + textareaScript.type = 'text/javascript'; + textareaScript.src = `${protocol}://${admin_chat_domain}/static/js/osichat-admin/textarea.js`; + conversationContainer.appendChild(textareaScript); + } + + function scrollBottom() { + const conversationContainer = document.getElementById('messages_container'); + if (conversationContainer) { + conversationContainer.scrollTo({ + top: conversationContainer.scrollHeight, + behavior: 'smooth' + }); + } + } + + function openConversation(sessionId, chatId) { + if (osichatadminroomSocket) { + osichatadminroomSocket.close(); + } + + osichatadminroomSocket = new WebSocket(`${admin_chat_ws_scheme}://${admin_chat_domain}/ws/osichat-admin/${sessionId}/${chatId}/`); + + osichatadminroomSocket.onopen = function () { + scrollBottom(); + hideLoader(); + markCurrentChatRead(chatId) + console.log('WebSocket connection to osichat established'); + osichatadminroomSocket.send(JSON.stringify({ 'event_type': 'load_chat', 'client_type': 'website_admin' })); + osichatadminroomSocket.send(JSON.stringify({ 'event_type': 'update_read_messages', 'user_id': userId, 'chat_state': 'open' })); + }; + + function handleLoadChatEvent(data) { + let chatDiv = document.getElementById('widgetRightSide'); + chatDiv.innerHTML = data.html; + appendTextAreaScript(chatDiv); + + const sendMessageForm = document.querySelector('#sendMessage'); + sendMessageForm.addEventListener('submit', function (event) { + event.preventDefault(); + const message = event.target.elements.message.value; + const eventMessage = { + 'event_type': 'send_message', + 'message': message, + 'user_id': userId + }; + + osichatadminroomSocket.send(JSON.stringify(eventMessage)); + event.target.reset(); + }); + } + + osichatadminroomSocket.onmessage = function (e) { + const data = JSON.parse(e.data); + const typingDiv = document.getElementById('typing'); + const messagesDiv = document.getElementById('messages_container'); + switch (data.event_type) { + case 'load_chat': + handleLoadChatEvent(data); + break; + case 'typing': + if (!typingDiv && data.user != userId) { + messagesDiv.insertAdjacentHTML('beforeend', data.html); + } + break; + case 'stopped_typing': + if (typingDiv) { + typingDiv.remove(); + } + break; + case 'send_message': + osichatadminroomSocket.send(JSON.stringify({ 'event_type': 'update_read_messages', 'user_id': userId, 'chat_state': 'open' })); + messagesDiv.insertAdjacentHTML('beforeend', data.html); + if (!data.user){ if (typingDiv) { typingDiv.remove(); } - break; - case 'send_message': - osichatadminroomSocket.send(JSON.stringify({ 'event_type': 'update_read_messages', 'user_id': userId, 'chat_state': 'open' })); - messagesDiv.insertAdjacentHTML('beforeend', data.html); - if (!data.user) { // If it is sent by a guest play a notification sound for the guest - const notificationSound = document.getElementById('notification-sound'); - notificationSound.play(); - if (typingDiv) { - typingDiv.remove(); - } - break; - } - break; - default: - console.log('Unknown event type:', data.event_type); - } - }; - - osichatadminroomSocket.onclose = function () { - console.log('WebSocket connection to osichat closed'); - }; - - osichatadminroomSocket.onerror = function (error) { - console.error('WebSocket error:', error); - }; + } + break; + default: + console.log('Unknown event type:', data.event_type); + } + }; + + osichatadminroomSocket.onclose = function () { + console.log('WebSocket connection closed'); + if (currentChatId === chatId) { + setTimeout(() => { + console.log('Attempting to reconnect to WebSocket...'); + openConversation(sessionId, chatId); + }, 2000); + } + }; + + osichatadminroomSocket.onerror = function (error) { + console.error('WebSocket error:', error); + showLoader(); + }; } -})(); \ No newline at end of file + + window.addEventListener('offline', () => { + showLoader(); + if (osichatadminroomSocket) { + osichatadminroomSocket.close(); + } + }); + +})(); diff --git a/osinaweb/static/js/osichat-admin/rooms.js b/osinaweb/static/js/osichat-admin/rooms.js index 1ddea089..28174bbe 100644 --- a/osinaweb/static/js/osichat-admin/rooms.js +++ b/osinaweb/static/js/osichat-admin/rooms.js @@ -2,8 +2,24 @@ let admin_chat_ws_scheme = window.location.protocol === "https:" ? "wss" : "ws"; let protocol = window.location.protocol === "https:" ? "https" : "http"; let admin_chat_domain = "osina.ositcom.com"; let userId = document.getElementById('userId').textContent.trim(); -let osichatroomsSocket; -let osichatadminroomSocket; +let osichatadminroomSocket = null; +let currentChatId = null; + +// FUNCTIONS TO SHOW & HIDE THE LOADER +function showLoader() { + const roomsLoader = document.getElementById('roomsLoader'); + if (roomsLoader) { + roomsLoader.classList.remove('hidden'); + } +} + +function hideLoader() { + const roomsLoader = document.getElementById('roomsLoader'); + if (roomsLoader) { + roomsLoader.classList.add('hidden'); + } +} + function appendInnerConversationScript(div) { const innerConversationScript = document.createElement('script'); @@ -12,11 +28,16 @@ function appendInnerConversationScript(div) { div.appendChild(innerConversationScript); } + + + + function getRooms(){ osichatroomsSocket = new WebSocket(`${admin_chat_ws_scheme}://${admin_chat_domain}/ws/osichat/rooms/`); osichatroomsSocket.onopen = function () { console.log('WebSocket connection to rooms established'); + hideLoader(); osichatroomsSocket.send(JSON.stringify({ 'event_type': 'set_client_type', 'client_type': 'website_admin', @@ -39,11 +60,34 @@ function getRooms(){ if (chatRoomDiv) { chatRoomDiv.remove(); // Remove the existing chat room div } + // Insert the new chat room HTML at the top of the container const newChatRoomDiv = document.createElement('div'); newChatRoomDiv.innerHTML = data.html; roomsContainer.insertAdjacentElement('afterbegin', newChatRoomDiv.firstElementChild); + if (parseInt(currentChatId) === parseInt(data.chatroom_id)) { + const unreadIndicator = roomsContainer.querySelector(`.unread[data-roomid='${data.chatroom_id}']`); + if (unreadIndicator) { + unreadIndicator.classList.add('hidden'); + } + if (!data.user) { // If it is sent by a guest play a notification sound for the guest + const notificationSound = document.getElementById('notification-sound'); + notificationSound.play(); + + } + } else { + const unreadIndicator = roomsContainer.querySelector(`.unread[data-roomid='${data.chatroom_id}']`); + if (unreadIndicator) { + unreadIndicator.classList.remove('hidden'); + } + if (!data.user) { // If it is sent by a guest play a notification sound for the guest + const notificationSound = document.getElementById('notification-sound'); + notificationSound.play(); + } + } + + appendInnerConversationScript(leftDiv); break; @@ -53,12 +97,25 @@ function getRooms(){ }; osichatroomsSocket.onclose = function () { - console.log('WebSocket connection to rooms closed'); + showLoader(); + setTimeout(() => { + console.log('Attempting to reconnect to WebSocket...'); + getRooms(); + }, 2000); }; osichatroomsSocket.onerror = function (error) { + showLoader(); console.error('WebSocket error:', error); }; + + window.addEventListener('offline', () => { + showLoader(); + if (osichatroomsSocket) { + osichatroomsSocket.close(); + } + }); + } getRooms(); \ No newline at end of file diff --git a/osinaweb/static/js/osichat-admin/textarea.js b/osinaweb/static/js/osichat-admin/textarea.js index 066ed475..c8840d12 100644 --- a/osinaweb/static/js/osichat-admin/textarea.js +++ b/osinaweb/static/js/osichat-admin/textarea.js @@ -1,12 +1,14 @@ (function() { // FUNCTION TO ADJUST TEXTAREA HEIGHT AND SUBMIT BUTTON VISIBILITY function adjustTextAreaAndButton(textarea, submitButton) { + // Adjust the height of the textarea if (textarea.value.trim() === '') { textarea.style.height = '50px'; } else { textarea.style.height = textarea.scrollHeight + 'px'; } + // Display and hide the submit button if (textarea.value.trim() === '') { submitButton.classList.add('hidden'); } else { @@ -14,10 +16,21 @@ } } + function scrollBottom() { + const conversationContainer = document.getElementById('messages_container'); + + if (conversationContainer) { + conversationContainer.scrollTo({ + top: conversationContainer.scrollHeight, + behavior: 'smooth' + }); + } + } + + // INITIALIZE ELEMENTS const form = document.querySelector('#sendMessage'); const textarea = document.querySelector('#dynamicTextarea'); - const conversationContainer = document.getElementById('conversation'); const submitButton = document.getElementById('submitMessageButton'); const typingUserId = document.getElementById('userId').textContent.trim(); let typingTimeout; @@ -54,8 +67,18 @@ submitButton.classList.add('hidden'); setTimeout(() => { - conversationContainer.scrollTop = conversationContainer.scrollHeight; + scrollBottom(); }, 100); }); + textarea.addEventListener('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + if (!submitButton.classList.contains('hidden')) { + submitButton.click(); + } + scrollBottom(); + } + }); + })(); diff --git a/osinaweb/static/js/osichat/textarea.js b/osinaweb/static/js/osichat/textarea.js index 55aaead6..b177b473 100644 --- a/osinaweb/static/js/osichat/textarea.js +++ b/osinaweb/static/js/osichat/textarea.js @@ -18,15 +18,19 @@ function scrollBottom() { const conversationContainer = document.getElementById('conversation'); - conversationContainer.scrollTop = conversationContainer.scrollHeight; + + if (conversationContainer) { + conversationContainer.scrollTo({ + top: conversationContainer.scrollHeight, + behavior: 'smooth' + }); + } } - // INITIALIZE ELEMENTS const form = document.querySelector('#sendMessage'); const textarea = document.querySelector('#dynamicTextarea'); - const conversationContainer = document.getElementById('conversation'); const submitButton = document.getElementById('submitMessageButton'); let typingTimeout; let isTyping = false;