From c6a735088014a5103370fbc0707d0514e76fcff7 Mon Sep 17 00:00:00 2001 From: falbue Date: Sat, 8 Nov 2025 17:19:10 +0500 Subject: [PATCH 1/9] =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D0=BD=D0=B0=D0=B4=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D0=B5=D0=B9=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 9 +++ static/css/files.css | 72 ++++++++++++++++++ static/scripts/chat.js | 155 ++++++++++++++++++++++++++++++++++++++ static/scripts/message.js | 136 +++++++++++++++++++++++++++++++++ templates/base.html | 96 ++++++++++------------- templates/chat.html | 19 ++--- 6 files changed, 421 insertions(+), 66 deletions(-) create mode 100644 static/css/files.css create mode 100644 static/scripts/message.js diff --git a/app.py b/app.py index df26acd..acf41c0 100644 --- a/app.py +++ b/app.py @@ -60,5 +60,14 @@ def handle_ice_candidate(data): socketio.emit("webrtc:ice-candidate", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] +@socketio.on("update_attachments") +def handle_update_attachments(data): + """При получении списка прикреплённых файлов от клиента — рассылка остальным в комнате.""" + chat_id = data.get("chat_id") + if not chat_id: + return + socketio.emit("receive_attachments", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] + + if __name__ == "__main__": socketio.run(app, host="0.0.0.0", port=80, debug=True) diff --git a/static/css/files.css b/static/css/files.css new file mode 100644 index 0000000..9021403 --- /dev/null +++ b/static/css/files.css @@ -0,0 +1,72 @@ +/* Стили для UI прикрепления файлов */ +.attachments { + display: flex; + gap: 8px; + padding: 8px 12px; + flex-wrap: wrap; + box-sizing: border-box; +} + +.attachment { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + background: rgba(0, 0, 0, 0.04); + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.06); + min-width: 120px; +} + +.attachment.received { + background: rgba(0, 0, 0, 0.02); +} + +.attachment-preview { + width: 56px; + height: 56px; + object-fit: cover; + border-radius: 6px; +} + +.attachment-meta { + display: flex; + flex-direction: column; + font-size: 0.9rem; +} + +.attachment-name { + font-weight: 600; + color: var(--text-color); +} + +.attachment-size { + font-size: 0.8rem; + color: var(--text-color); + opacity: 0.7; +} + +.attachment-remove { + margin-left: auto; + background: transparent; + border: none; + color: var(--text-color); + font-size: 1.2rem; + cursor: pointer; +} + +.textarea-wrapper.drag-over { + outline: 2px dashed rgba(0, 0, 0, 0.15); + border-radius: 8px; +} + +.received-attachments { + padding: 6px 12px; +} + +.attachment-download { + margin-left: 8px; + font-size: 0.9rem; + color: var(--text-color); + text-decoration: underline; +} \ No newline at end of file diff --git a/static/scripts/chat.js b/static/scripts/chat.js index 801f51b..1d9e9f9 100644 --- a/static/scripts/chat.js +++ b/static/scripts/chat.js @@ -3,6 +3,11 @@ const chatId = window.location.pathname.split("/").pop(); const inputMessage = document.getElementById("inputMessage"); const displayMessage = document.getElementById("displayMessage"); const senderId = Math.random().toString(36).substr(2, 9); +const attachButton = document.getElementById("attachButton"); +const fileInput = document.getElementById("fileInput"); +const attachmentsContainer = document.getElementById("attachments"); + +let attachments = []; // локальные прикрепления пользователя function formatMessage(message) { const escapeHtml = (str) => { @@ -37,6 +42,156 @@ socket.on("receive_message", (data) => { } }); +// Обработка входящих прикреплённых файлов от других пользователей +socket.on("receive_attachments", (data) => { + if (data.sender_id === senderId) return; + if (!data.attachments || !data.attachments.length) return; + + // Отобразим прикрепления в области сообщений (append) + const container = document.createElement("div"); + container.className = "received-attachments"; + + data.attachments.forEach((a) => { + const item = document.createElement("div"); + item.className = "attachment received"; + + if (a.type && a.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = a.dataUrl; + img.alt = a.name; + img.className = "attachment-preview"; + item.appendChild(img); + } + + const info = document.createElement("div"); + info.className = "attachment-info"; + info.innerHTML = `
${a.name}
${Math.round(a.size / 1024)} KB
`; + item.appendChild(info); + + const link = document.createElement("a"); + link.href = a.dataUrl; + link.download = a.name; + link.textContent = "Скачать"; + link.className = "attachment-download"; + item.appendChild(link); + + container.appendChild(item); + }); + + // Вставим контейнер в область чата + const chat = document.querySelector('.chat'); + if (chat) chat.appendChild(container); +}); + +function renderAttachments() { + attachmentsContainer.innerHTML = ""; + attachments.forEach((att) => { + const el = document.createElement("div"); + el.className = "attachment"; + el.dataset.id = att.id; + + if (att.type && att.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = att.dataUrl; + img.alt = att.name; + img.className = "attachment-preview"; + el.appendChild(img); + } else { + const icon = document.createElement("i"); + icon.className = "iconoir-attachment"; + icon.style.fontSize = '1.1rem'; + el.appendChild(icon); + } + + const meta = document.createElement("div"); + meta.className = "attachment-meta"; + meta.innerHTML = `
${att.name}
${Math.round(att.size / 1024)} KB
`; + el.appendChild(meta); + + const removeBtn = document.createElement("button"); + removeBtn.type = 'button'; + removeBtn.className = 'attachment-remove'; + removeBtn.title = 'Удалить файл'; + removeBtn.textContent = '×'; + removeBtn.addEventListener('click', () => { + removeAttachment(att.id); + }); + el.appendChild(removeBtn); + + attachmentsContainer.appendChild(el); + }); +} + +function emitAttachmentsUpdate() { + const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); + socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); +} + +function removeAttachment(id) { + attachments = attachments.filter((a) => a.id !== id); + renderAttachments(); + emitAttachmentsUpdate(); +} + +function handleFiles(fileList) { + const files = Array.from(fileList); + const readers = files.map((file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + const id = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); + const att = { id, name: file.name, size: file.size, type: file.type, dataUrl: e.target.result }; + attachments.push(att); + resolve(att); + }; + reader.readAsDataURL(file); + }); + }); + + Promise.all(readers).then(() => { + renderAttachments(); + emitAttachmentsUpdate(); + }); +} + +// Кнопка прикрепления +attachButton?.addEventListener('click', () => { + fileInput?.click(); +}); + +fileInput?.addEventListener('change', (e) => { + if (e.target.files && e.target.files.length) { + handleFiles(e.target.files); + e.target.value = null; + } +}); + +// Drag & drop на области ввода +const textareaWrapper = document.querySelector('.textarea-wrapper'); +if (textareaWrapper) { + ['dragenter', 'dragover'].forEach((ev) => { + textareaWrapper.addEventListener(ev, (e) => { + e.preventDefault(); + e.stopPropagation(); + textareaWrapper.classList.add('drag-over'); + }); + }); + ['dragleave', 'drop'].forEach((ev) => { + textareaWrapper.addEventListener(ev, (e) => { + e.preventDefault(); + e.stopPropagation(); + textareaWrapper.classList.remove('drag-over'); + }); + }); + + textareaWrapper.addEventListener('drop', (e) => { + const dt = e.dataTransfer; + if (dt && dt.files && dt.files.length) { + handleFiles(dt.files); + } + }); +} + function typeText(elementId, text) { const element = document.querySelector(`#${elementId} b`); let i = 0; diff --git a/static/scripts/message.js b/static/scripts/message.js new file mode 100644 index 0000000..5ae61c9 --- /dev/null +++ b/static/scripts/message.js @@ -0,0 +1,136 @@ +const socket = (window.socket = io()); +const chatId = window.location.pathname.split("/").pop(); +const inputMessage = document.getElementById("inputMessage"); +const displayMessage = document.getElementById("displayMessage"); +const senderId = Math.random().toString(36).substr(2, 9); + + + + + +function formatMessage(message) { + const escapeHtml = (str) => { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; + }; + let escapedMessage = escapeHtml(message); + + const codeBlockPattern = /```([\s\S]*?)```/g; + escapedMessage = escapedMessage.replace( + codeBlockPattern, + '
$1
', + ); + return escapedMessage.replace(/\n/g, "
"); +} + +socket.emit("join_chat", { chat_id: chatId, sender_id: senderId }); + +inputMessage.addEventListener("input", () => { + const messageText = inputMessage.value.trim() || "..."; + socket.emit("update_message", { + chat_id: chatId, + text: messageText, + sender_id: senderId, + }); +}); + +socket.on("receive_message", (data) => { + if (data.sender_id !== senderId) { + displayMessage.innerHTML = formatMessage(data.text); + } +}); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +function typeText(elementId, text) { + const element = document.querySelector(`#${elementId} b`); + let i = 0; + + const interval = setInterval(() => { + if (i < text.length) { + element.textContent += text.charAt(i); + i++; + } else { + clearInterval(interval); + } + }, 100); // Задержка между символами +} + +document.addEventListener("DOMContentLoaded", () => { + typeText("displayMessage", "Ожидание пользователя..."); +}); diff --git a/templates/base.html b/templates/base.html index 4ae4498..b898507 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,57 +1,43 @@ - - - - - {% block title %}{% endblock %} - - - - - - - - - - - - - - - - - - - - - - - - - {% block content %}{% endblock %} - - + + + + + + {% block title %}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + {% block content %}{% endblock %} + + + \ No newline at end of file diff --git a/templates/chat.html b/templates/chat.html index dd88287..e4bc469 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -28,19 +28,19 @@ +
+
+
- +
@@ -49,10 +49,7 @@ - + -{% endblock %} +{% endblock %} \ No newline at end of file From 1c87d0fc1313f8a1de0d61afd47d660d95d76dc3 Mon Sep 17 00:00:00 2001 From: falbue Date: Sat, 8 Nov 2025 17:23:15 +0500 Subject: [PATCH 2/9] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D1=81=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=BC=20=D0=B8=20=D1=87?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=BC,=20=D1=80=D0=B0=D0=B7=D0=B1=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B7=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/scripts/chat.js | 211 -------------------------------------- static/scripts/files.js | 157 ++++++++++++++++++++++++++++ static/scripts/message.js | 71 ++++++------- templates/chat.html | 3 +- 4 files changed, 193 insertions(+), 249 deletions(-) delete mode 100644 static/scripts/chat.js create mode 100644 static/scripts/files.js diff --git a/static/scripts/chat.js b/static/scripts/chat.js deleted file mode 100644 index 1d9e9f9..0000000 --- a/static/scripts/chat.js +++ /dev/null @@ -1,211 +0,0 @@ -const socket = (window.socket = io()); -const chatId = window.location.pathname.split("/").pop(); -const inputMessage = document.getElementById("inputMessage"); -const displayMessage = document.getElementById("displayMessage"); -const senderId = Math.random().toString(36).substr(2, 9); -const attachButton = document.getElementById("attachButton"); -const fileInput = document.getElementById("fileInput"); -const attachmentsContainer = document.getElementById("attachments"); - -let attachments = []; // локальные прикрепления пользователя - -function formatMessage(message) { - const escapeHtml = (str) => { - const div = document.createElement("div"); - div.textContent = str; - return div.innerHTML; - }; - let escapedMessage = escapeHtml(message); - - const codeBlockPattern = /```([\s\S]*?)```/g; - escapedMessage = escapedMessage.replace( - codeBlockPattern, - '
$1
', - ); - return escapedMessage.replace(/\n/g, "
"); -} - -socket.emit("join_chat", { chat_id: chatId, sender_id: senderId }); - -inputMessage.addEventListener("input", () => { - const messageText = inputMessage.value.trim() || "..."; - socket.emit("update_message", { - chat_id: chatId, - text: messageText, - sender_id: senderId, - }); -}); - -socket.on("receive_message", (data) => { - if (data.sender_id !== senderId) { - displayMessage.innerHTML = formatMessage(data.text); - } -}); - -// Обработка входящих прикреплённых файлов от других пользователей -socket.on("receive_attachments", (data) => { - if (data.sender_id === senderId) return; - if (!data.attachments || !data.attachments.length) return; - - // Отобразим прикрепления в области сообщений (append) - const container = document.createElement("div"); - container.className = "received-attachments"; - - data.attachments.forEach((a) => { - const item = document.createElement("div"); - item.className = "attachment received"; - - if (a.type && a.type.startsWith("image/")) { - const img = document.createElement("img"); - img.src = a.dataUrl; - img.alt = a.name; - img.className = "attachment-preview"; - item.appendChild(img); - } - - const info = document.createElement("div"); - info.className = "attachment-info"; - info.innerHTML = `
${a.name}
${Math.round(a.size / 1024)} KB
`; - item.appendChild(info); - - const link = document.createElement("a"); - link.href = a.dataUrl; - link.download = a.name; - link.textContent = "Скачать"; - link.className = "attachment-download"; - item.appendChild(link); - - container.appendChild(item); - }); - - // Вставим контейнер в область чата - const chat = document.querySelector('.chat'); - if (chat) chat.appendChild(container); -}); - -function renderAttachments() { - attachmentsContainer.innerHTML = ""; - attachments.forEach((att) => { - const el = document.createElement("div"); - el.className = "attachment"; - el.dataset.id = att.id; - - if (att.type && att.type.startsWith("image/")) { - const img = document.createElement("img"); - img.src = att.dataUrl; - img.alt = att.name; - img.className = "attachment-preview"; - el.appendChild(img); - } else { - const icon = document.createElement("i"); - icon.className = "iconoir-attachment"; - icon.style.fontSize = '1.1rem'; - el.appendChild(icon); - } - - const meta = document.createElement("div"); - meta.className = "attachment-meta"; - meta.innerHTML = `
${att.name}
${Math.round(att.size / 1024)} KB
`; - el.appendChild(meta); - - const removeBtn = document.createElement("button"); - removeBtn.type = 'button'; - removeBtn.className = 'attachment-remove'; - removeBtn.title = 'Удалить файл'; - removeBtn.textContent = '×'; - removeBtn.addEventListener('click', () => { - removeAttachment(att.id); - }); - el.appendChild(removeBtn); - - attachmentsContainer.appendChild(el); - }); -} - -function emitAttachmentsUpdate() { - const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); - socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); -} - -function removeAttachment(id) { - attachments = attachments.filter((a) => a.id !== id); - renderAttachments(); - emitAttachmentsUpdate(); -} - -function handleFiles(fileList) { - const files = Array.from(fileList); - const readers = files.map((file) => { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - const id = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); - const att = { id, name: file.name, size: file.size, type: file.type, dataUrl: e.target.result }; - attachments.push(att); - resolve(att); - }; - reader.readAsDataURL(file); - }); - }); - - Promise.all(readers).then(() => { - renderAttachments(); - emitAttachmentsUpdate(); - }); -} - -// Кнопка прикрепления -attachButton?.addEventListener('click', () => { - fileInput?.click(); -}); - -fileInput?.addEventListener('change', (e) => { - if (e.target.files && e.target.files.length) { - handleFiles(e.target.files); - e.target.value = null; - } -}); - -// Drag & drop на области ввода -const textareaWrapper = document.querySelector('.textarea-wrapper'); -if (textareaWrapper) { - ['dragenter', 'dragover'].forEach((ev) => { - textareaWrapper.addEventListener(ev, (e) => { - e.preventDefault(); - e.stopPropagation(); - textareaWrapper.classList.add('drag-over'); - }); - }); - ['dragleave', 'drop'].forEach((ev) => { - textareaWrapper.addEventListener(ev, (e) => { - e.preventDefault(); - e.stopPropagation(); - textareaWrapper.classList.remove('drag-over'); - }); - }); - - textareaWrapper.addEventListener('drop', (e) => { - const dt = e.dataTransfer; - if (dt && dt.files && dt.files.length) { - handleFiles(dt.files); - } - }); -} - -function typeText(elementId, text) { - const element = document.querySelector(`#${elementId} b`); - let i = 0; - - const interval = setInterval(() => { - if (i < text.length) { - element.textContent += text.charAt(i); - i++; - } else { - clearInterval(interval); - } - }, 100); // Задержка между символами -} - -document.addEventListener("DOMContentLoaded", () => { - typeText("displayMessage", "Ожидание пользователя..."); -}); diff --git a/static/scripts/files.js b/static/scripts/files.js new file mode 100644 index 0000000..f3f1739 --- /dev/null +++ b/static/scripts/files.js @@ -0,0 +1,157 @@ +// Логика работы с прикреплениями (зависит от глобальных переменных: socket, chatId, senderId) +const attachButton = document.getElementById("attachButton"); +const fileInput = document.getElementById("fileInput"); +const attachmentsContainer = document.getElementById("attachments"); + +let attachments = []; // локальные прикрепления пользователя + +// Обработка входящих прикреплённых файлов от других пользователей +socket.on("receive_attachments", (data) => { + if (data.sender_id === senderId) return; + if (!data.attachments || !data.attachments.length) return; + + // Отобразим прикрепления в области сообщений (append) + const container = document.createElement("div"); + container.className = "received-attachments"; + + data.attachments.forEach((a) => { + const item = document.createElement("div"); + item.className = "attachment received"; + + if (a.type && a.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = a.dataUrl; + img.alt = a.name; + img.className = "attachment-preview"; + item.appendChild(img); + } + + const info = document.createElement("div"); + info.className = "attachment-info"; + info.innerHTML = `
${a.name}
${Math.round(a.size / 1024)} KB
`; + item.appendChild(info); + + const link = document.createElement("a"); + link.href = a.dataUrl; + link.download = a.name; + link.textContent = "Скачать"; + link.className = "attachment-download"; + item.appendChild(link); + + container.appendChild(item); + }); + + // Вставим контейнер в область чата + const chat = document.querySelector('.chat'); + if (chat) chat.appendChild(container); +}); + +function renderAttachments() { + if (!attachmentsContainer) return; + attachmentsContainer.innerHTML = ""; + attachments.forEach((att) => { + const el = document.createElement("div"); + el.className = "attachment"; + el.dataset.id = att.id; + + if (att.type && att.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = att.dataUrl; + img.alt = att.name; + img.className = "attachment-preview"; + el.appendChild(img); + } else { + const icon = document.createElement("i"); + icon.className = "iconoir-attachment"; + icon.style.fontSize = '1.1rem'; + el.appendChild(icon); + } + + const meta = document.createElement("div"); + meta.className = "attachment-meta"; + meta.innerHTML = `
${att.name}
${Math.round(att.size / 1024)} KB
`; + el.appendChild(meta); + + const removeBtn = document.createElement("button"); + removeBtn.type = 'button'; + removeBtn.className = 'attachment-remove'; + removeBtn.title = 'Удалить файл'; + removeBtn.textContent = '×'; + removeBtn.addEventListener('click', () => { + removeAttachment(att.id); + }); + el.appendChild(removeBtn); + + attachmentsContainer.appendChild(el); + }); +} + +function emitAttachmentsUpdate() { + const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); + socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); +} + +function removeAttachment(id) { + attachments = attachments.filter((a) => a.id !== id); + renderAttachments(); + emitAttachmentsUpdate(); +} + +function handleFiles(fileList) { + const files = Array.from(fileList); + const readers = files.map((file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + const id = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); + const att = { id, name: file.name, size: file.size, type: file.type, dataUrl: e.target.result }; + attachments.push(att); + resolve(att); + }; + reader.readAsDataURL(file); + }); + }); + + Promise.all(readers).then(() => { + renderAttachments(); + emitAttachmentsUpdate(); + }); +} + +// Кнопка прикрепления +attachButton?.addEventListener('click', () => { + fileInput?.click(); +}); + +fileInput?.addEventListener('change', (e) => { + if (e.target.files && e.target.files.length) { + handleFiles(e.target.files); + e.target.value = null; + } +}); + +// Drag & drop на области ввода +const textareaWrapper = document.querySelector('.textarea-wrapper'); +if (textareaWrapper) { + ['dragenter', 'dragover'].forEach((ev) => { + textareaWrapper.addEventListener(ev, (e) => { + e.preventDefault(); + e.stopPropagation(); + textareaWrapper.classList.add('drag-over'); + }); + }); + ['dragleave', 'drop'].forEach((ev) => { + textareaWrapper.addEventListener(ev, (e) => { + e.preventDefault(); + e.stopPropagation(); + textareaWrapper.classList.remove('drag-over'); + }); + }); + + textareaWrapper.addEventListener('drop', (e) => { + const dt = e.dataTransfer; + if (dt && dt.files && dt.files.length) { + handleFiles(dt.files); + } + }); +} diff --git a/static/scripts/message.js b/static/scripts/message.js index 5ae61c9..b2238f9 100644 --- a/static/scripts/message.js +++ b/static/scripts/message.js @@ -5,40 +5,37 @@ const displayMessage = document.getElementById("displayMessage"); const senderId = Math.random().toString(36).substr(2, 9); - - - function formatMessage(message) { - const escapeHtml = (str) => { - const div = document.createElement("div"); - div.textContent = str; - return div.innerHTML; - }; - let escapedMessage = escapeHtml(message); - - const codeBlockPattern = /```([\s\S]*?)```/g; - escapedMessage = escapedMessage.replace( - codeBlockPattern, - '
$1
', - ); - return escapedMessage.replace(/\n/g, "
"); + const escapeHtml = (str) => { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; + }; + let escapedMessage = escapeHtml(message); + + const codeBlockPattern = /```([\s\S]*?)```/g; + escapedMessage = escapedMessage.replace( + codeBlockPattern, + '
$1
', + ); + return escapedMessage.replace(/\n/g, "
"); } socket.emit("join_chat", { chat_id: chatId, sender_id: senderId }); inputMessage.addEventListener("input", () => { - const messageText = inputMessage.value.trim() || "..."; - socket.emit("update_message", { - chat_id: chatId, - text: messageText, - sender_id: senderId, - }); + const messageText = inputMessage.value.trim() || "..."; + socket.emit("update_message", { + chat_id: chatId, + text: messageText, + sender_id: senderId, + }); }); socket.on("receive_message", (data) => { - if (data.sender_id !== senderId) { - displayMessage.innerHTML = formatMessage(data.text); - } + if (data.sender_id !== senderId) { + displayMessage.innerHTML = formatMessage(data.text); + } }); @@ -118,19 +115,19 @@ socket.on("receive_message", (data) => { function typeText(elementId, text) { - const element = document.querySelector(`#${elementId} b`); - let i = 0; - - const interval = setInterval(() => { - if (i < text.length) { - element.textContent += text.charAt(i); - i++; - } else { - clearInterval(interval); - } - }, 100); // Задержка между символами + const element = document.querySelector(`#${elementId} b`); + let i = 0; + + const interval = setInterval(() => { + if (i < text.length) { + element.textContent += text.charAt(i); + i++; + } else { + clearInterval(interval); + } + }, 100); // Задержка между символами } document.addEventListener("DOMContentLoaded", () => { - typeText("displayMessage", "Ожидание пользователя..."); + typeText("displayMessage", "Ожидание пользователя..."); }); diff --git a/templates/chat.html b/templates/chat.html index e4bc469..d644a06 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -45,7 +45,8 @@ - + + From 5140fae19bbd590a31a190e0df867fad729aa3ab Mon Sep 17 00:00:00 2001 From: falbue Date: Sat, 8 Nov 2025 17:29:42 +0500 Subject: [PATCH 3/9] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=B8=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/scripts/files.js | 286 +++++++++++++++++++++------------------- 1 file changed, 152 insertions(+), 134 deletions(-) diff --git a/static/scripts/files.js b/static/scripts/files.js index f3f1739..f65b45b 100644 --- a/static/scripts/files.js +++ b/static/scripts/files.js @@ -1,157 +1,175 @@ // Логика работы с прикреплениями (зависит от глобальных переменных: socket, chatId, senderId) -const attachButton = document.getElementById("attachButton"); -const fileInput = document.getElementById("fileInput"); -const attachmentsContainer = document.getElementById("attachments"); - -let attachments = []; // локальные прикрепления пользователя - -// Обработка входящих прикреплённых файлов от других пользователей -socket.on("receive_attachments", (data) => { - if (data.sender_id === senderId) return; - if (!data.attachments || !data.attachments.length) return; - - // Отобразим прикрепления в области сообщений (append) - const container = document.createElement("div"); - container.className = "received-attachments"; - - data.attachments.forEach((a) => { - const item = document.createElement("div"); - item.className = "attachment received"; - - if (a.type && a.type.startsWith("image/")) { - const img = document.createElement("img"); - img.src = a.dataUrl; - img.alt = a.name; - img.className = "attachment-preview"; - item.appendChild(img); - } +document.addEventListener('DOMContentLoaded', () => { + const attachButton = document.getElementById("attachButton"); + const fileInput = document.getElementById("fileInput"); + const attachmentsContainer = document.getElementById("attachments"); - const info = document.createElement("div"); - info.className = "attachment-info"; - info.innerHTML = `
${a.name}
${Math.round(a.size / 1024)} KB
`; - item.appendChild(info); + let attachments = []; // локальные прикрепления пользователя - const link = document.createElement("a"); - link.href = a.dataUrl; - link.download = a.name; - link.textContent = "Скачать"; - link.className = "attachment-download"; - item.appendChild(link); + // Обработка входящих прикреплённых файлов от других пользователей + socket.on("receive_attachments", (data) => { + // ожидаем, что сервер пересылает { chat_id, sender_id, attachments: [...] } + const sender = data.sender_id; - container.appendChild(item); - }); + // Найдём уже отрисованный контейнер от этого отправителя (если был) + const chat = document.querySelector('.chat'); + if (!chat) return; - // Вставим контейнер в область чата - const chat = document.querySelector('.chat'); - if (chat) chat.appendChild(container); -}); + const existing = chat.querySelector(`.received-attachments[data-sender="${sender}"]`); -function renderAttachments() { - if (!attachmentsContainer) return; - attachmentsContainer.innerHTML = ""; - attachments.forEach((att) => { - const el = document.createElement("div"); - el.className = "attachment"; - el.dataset.id = att.id; - - if (att.type && att.type.startsWith("image/")) { - const img = document.createElement("img"); - img.src = att.dataUrl; - img.alt = att.name; - img.className = "attachment-preview"; - el.appendChild(img); - } else { - const icon = document.createElement("i"); - icon.className = "iconoir-attachment"; - icon.style.fontSize = '1.1rem'; - el.appendChild(icon); + // Если нет прикреплений — удалить существующий контейнер (если есть) + if (!data.attachments || !data.attachments.length) { + if (existing) existing.remove(); + return; } - const meta = document.createElement("div"); - meta.className = "attachment-meta"; - meta.innerHTML = `
${att.name}
${Math.round(att.size / 1024)} KB
`; - el.appendChild(meta); - - const removeBtn = document.createElement("button"); - removeBtn.type = 'button'; - removeBtn.className = 'attachment-remove'; - removeBtn.title = 'Удалить файл'; - removeBtn.textContent = '×'; - removeBtn.addEventListener('click', () => { - removeAttachment(att.id); + // Создадим новый контейнер + const container = document.createElement("div"); + container.className = "received-attachments"; + container.dataset.sender = sender; + + data.attachments.forEach((a) => { + const item = document.createElement("div"); + item.className = "attachment received"; + + if (a.type && a.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = a.dataUrl; + img.alt = a.name; + img.className = "attachment-preview"; + item.appendChild(img); + } + + const info = document.createElement("div"); + info.className = "attachment-info"; + info.innerHTML = `
${a.name}
${Math.round(a.size / 1024)} KB
`; + item.appendChild(info); + + const link = document.createElement("a"); + link.href = a.dataUrl; + link.download = a.name; + link.textContent = "Скачать"; + link.className = "attachment-download"; + item.appendChild(link); + + container.appendChild(item); }); - el.appendChild(removeBtn); - attachmentsContainer.appendChild(el); + // Если уже есть — заменим, иначе добавим + if (existing) { + existing.replaceWith(container); + } else { + chat.appendChild(container); + } }); -} - -function emitAttachmentsUpdate() { - const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); - socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); -} - -function removeAttachment(id) { - attachments = attachments.filter((a) => a.id !== id); - renderAttachments(); - emitAttachmentsUpdate(); -} - -function handleFiles(fileList) { - const files = Array.from(fileList); - const readers = files.map((file) => { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - const id = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); - const att = { id, name: file.name, size: file.size, type: file.type, dataUrl: e.target.result }; - attachments.push(att); - resolve(att); - }; - reader.readAsDataURL(file); + + function renderAttachments() { + if (!attachmentsContainer) return; + attachmentsContainer.innerHTML = ""; + attachments.forEach((att) => { + const el = document.createElement("div"); + el.className = "attachment"; + el.dataset.id = att.id; + + if (att.type && att.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = att.dataUrl; + img.alt = att.name; + img.className = "attachment-preview"; + el.appendChild(img); + } else { + const icon = document.createElement("i"); + icon.className = "iconoir-attachment"; + icon.style.fontSize = '1.1rem'; + el.appendChild(icon); + } + + const meta = document.createElement("div"); + meta.className = "attachment-meta"; + meta.innerHTML = `
${att.name}
${Math.round(att.size / 1024)} KB
`; + el.appendChild(meta); + + const removeBtn = document.createElement("button"); + removeBtn.type = 'button'; + removeBtn.className = 'attachment-remove'; + removeBtn.title = 'Удалить файл'; + removeBtn.textContent = '×'; + removeBtn.addEventListener('click', () => { + removeAttachment(att.id); + }); + el.appendChild(removeBtn); + + attachmentsContainer.appendChild(el); }); - }); + } + + function emitAttachmentsUpdate() { + const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); + socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); + } - Promise.all(readers).then(() => { + function removeAttachment(id) { + attachments = attachments.filter((a) => a.id !== id); renderAttachments(); emitAttachmentsUpdate(); - }); -} - -// Кнопка прикрепления -attachButton?.addEventListener('click', () => { - fileInput?.click(); -}); - -fileInput?.addEventListener('change', (e) => { - if (e.target.files && e.target.files.length) { - handleFiles(e.target.files); - e.target.value = null; } -}); -// Drag & drop на области ввода -const textareaWrapper = document.querySelector('.textarea-wrapper'); -if (textareaWrapper) { - ['dragenter', 'dragover'].forEach((ev) => { - textareaWrapper.addEventListener(ev, (e) => { - e.preventDefault(); - e.stopPropagation(); - textareaWrapper.classList.add('drag-over'); + function handleFiles(fileList) { + const files = Array.from(fileList); + const readers = files.map((file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + const id = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); + const att = { id, name: file.name, size: file.size, type: file.type, dataUrl: e.target.result }; + attachments.push(att); + resolve(att); + }; + reader.readAsDataURL(file); + }); }); - }); - ['dragleave', 'drop'].forEach((ev) => { - textareaWrapper.addEventListener(ev, (e) => { - e.preventDefault(); - e.stopPropagation(); - textareaWrapper.classList.remove('drag-over'); + + Promise.all(readers).then(() => { + renderAttachments(); + emitAttachmentsUpdate(); }); + } + + // Кнопка прикрепления + attachButton?.addEventListener('click', () => { + fileInput?.click(); }); - textareaWrapper.addEventListener('drop', (e) => { - const dt = e.dataTransfer; - if (dt && dt.files && dt.files.length) { - handleFiles(dt.files); + fileInput?.addEventListener('change', (e) => { + if (e.target.files && e.target.files.length) { + handleFiles(e.target.files); + e.target.value = null; } }); -} + + // Drag & drop на области ввода + const textareaWrapper = document.querySelector('.textarea-wrapper'); + if (textareaWrapper) { + ['dragenter', 'dragover'].forEach((ev) => { + textareaWrapper.addEventListener(ev, (e) => { + e.preventDefault(); + e.stopPropagation(); + textareaWrapper.classList.add('drag-over'); + }); + }); + ['dragleave', 'drop'].forEach((ev) => { + textareaWrapper.addEventListener(ev, (e) => { + e.preventDefault(); + e.stopPropagation(); + textareaWrapper.classList.remove('drag-over'); + }); + }); + + textareaWrapper.addEventListener('drop', (e) => { + const dt = e.dataTransfer; + if (dt && dt.files && dt.files.length) { + handleFiles(dt.files); + } + }); + } +}); From 0e090412618ec9d818ce34c92eb0d46378d07954 Mon Sep 17 00:00:00 2001 From: falbue Date: Sat, 8 Nov 2025 17:52:47 +0500 Subject: [PATCH 4/9] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0,=20?= =?UTF-8?q?=D1=81=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=BE=D0=B9?= =?UTF-8?q?=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 37 ++++++++++++++++++++++++++++++++++++- static/scripts/files.js | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index acf41c0..4d5b117 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,17 @@ import uuid +import os +import base64 -from flask import Flask, render_template, request +from flask import Flask, render_template, request, jsonify from flask_socketio import SocketIO, join_room app = Flask(__name__) + +# Папка для загруженных файлов +UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "static", "uploads") +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER + socketio = SocketIO(app, async_mode="eventlet") @@ -69,5 +77,32 @@ def handle_update_attachments(data): socketio.emit("receive_attachments", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] +@app.route("/upload", methods=["POST"]) +def upload_files(): + # Принимаем multipart/form-data с полем files (может быть несколько файлов) + # Изменено: не сохраняем файлы на диск. Читаем содержимое и возвращаем base64-данные. + files = request.files.getlist("files") + saved = [] + for f in files: + if not f or f.filename == "": + continue + # читаем содержимое в память + content = f.read() + size = len(content) + # кодируем в base64 для передачи в JSON; формируем data URL + b64 = base64.b64encode(content).decode("ascii") + mime = f.mimetype or "application/octet-stream" + data_url = f"data:{mime};base64,{b64}" + saved.append( + { + "name": f.filename, + "size": size, + "type": mime, + "data": data_url, + } + ) + return jsonify({"files": saved}) + + if __name__ == "__main__": socketio.run(app, host="0.0.0.0", port=80, debug=True) diff --git a/static/scripts/files.js b/static/scripts/files.js index f65b45b..49b89d3 100644 --- a/static/scripts/files.js +++ b/static/scripts/files.js @@ -34,7 +34,8 @@ document.addEventListener('DOMContentLoaded', () => { if (a.type && a.type.startsWith("image/")) { const img = document.createElement("img"); - img.src = a.dataUrl; + // Поддерживаем серверный URL или локальный dataUrl + img.src = a.dataUrl || a.url || ''; img.alt = a.name; img.className = "attachment-preview"; item.appendChild(img); @@ -46,7 +47,8 @@ document.addEventListener('DOMContentLoaded', () => { item.appendChild(info); const link = document.createElement("a"); - link.href = a.dataUrl; + // Ссылка на файл: серверный URL когда доступен, иначе dataUrl + link.href = a.url || a.dataUrl || '#'; link.download = a.name; link.textContent = "Скачать"; link.className = "attachment-download"; @@ -73,7 +75,7 @@ document.addEventListener('DOMContentLoaded', () => { if (att.type && att.type.startsWith("image/")) { const img = document.createElement("img"); - img.src = att.dataUrl; + img.src = att.dataUrl || att.url || ''; img.alt = att.name; img.className = "attachment-preview"; el.appendChild(img); @@ -104,7 +106,8 @@ document.addEventListener('DOMContentLoaded', () => { } function emitAttachmentsUpdate() { - const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); + // По возможности отправляем только метаданные и URL файлов, чтобы не перегружать socket + const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, url: a.url })); socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); } @@ -129,7 +132,31 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - Promise.all(readers).then(() => { + // После создания локальных превью — показываем их, затем загружаем файлы на сервер + Promise.all(readers).then(async () => { + renderAttachments(); + + try { + const form = new FormData(); + files.forEach((f) => form.append('files', f)); + const res = await fetch('/upload', { method: 'POST', body: form }); + if (res.ok) { + const data = await res.json(); + (data.files || []).forEach((u, idx) => { + const att = attachments[idx]; + if (att) { + att.url = u.url; + // удаляем dataUrl чтобы не отправлять большой base64 по sockets + delete att.dataUrl; + } + }); + } else { + console.warn('Upload failed', res.status); + } + } catch (err) { + console.error('Upload error', err); + } + renderAttachments(); emitAttachmentsUpdate(); }); From 0bd4468d762ffaeba97f9ed3664e167ca7f65d0b Mon Sep 17 00:00:00 2001 From: falbue Date: Sat, 8 Nov 2025 18:07:25 +0500 Subject: [PATCH 5/9] =?UTF-8?q?Revert=20"=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B0,=20=D1=81=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0e090412618ec9d818ce34c92eb0d46378d07954. --- app.py | 37 +------------------------------------ static/scripts/files.js | 37 +++++-------------------------------- 2 files changed, 6 insertions(+), 68 deletions(-) diff --git a/app.py b/app.py index 4d5b117..acf41c0 100644 --- a/app.py +++ b/app.py @@ -1,17 +1,9 @@ import uuid -import os -import base64 -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request from flask_socketio import SocketIO, join_room app = Flask(__name__) - -# Папка для загруженных файлов -UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "static", "uploads") -os.makedirs(UPLOAD_FOLDER, exist_ok=True) -app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER - socketio = SocketIO(app, async_mode="eventlet") @@ -77,32 +69,5 @@ def handle_update_attachments(data): socketio.emit("receive_attachments", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] -@app.route("/upload", methods=["POST"]) -def upload_files(): - # Принимаем multipart/form-data с полем files (может быть несколько файлов) - # Изменено: не сохраняем файлы на диск. Читаем содержимое и возвращаем base64-данные. - files = request.files.getlist("files") - saved = [] - for f in files: - if not f or f.filename == "": - continue - # читаем содержимое в память - content = f.read() - size = len(content) - # кодируем в base64 для передачи в JSON; формируем data URL - b64 = base64.b64encode(content).decode("ascii") - mime = f.mimetype or "application/octet-stream" - data_url = f"data:{mime};base64,{b64}" - saved.append( - { - "name": f.filename, - "size": size, - "type": mime, - "data": data_url, - } - ) - return jsonify({"files": saved}) - - if __name__ == "__main__": socketio.run(app, host="0.0.0.0", port=80, debug=True) diff --git a/static/scripts/files.js b/static/scripts/files.js index 49b89d3..f65b45b 100644 --- a/static/scripts/files.js +++ b/static/scripts/files.js @@ -34,8 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { if (a.type && a.type.startsWith("image/")) { const img = document.createElement("img"); - // Поддерживаем серверный URL или локальный dataUrl - img.src = a.dataUrl || a.url || ''; + img.src = a.dataUrl; img.alt = a.name; img.className = "attachment-preview"; item.appendChild(img); @@ -47,8 +46,7 @@ document.addEventListener('DOMContentLoaded', () => { item.appendChild(info); const link = document.createElement("a"); - // Ссылка на файл: серверный URL когда доступен, иначе dataUrl - link.href = a.url || a.dataUrl || '#'; + link.href = a.dataUrl; link.download = a.name; link.textContent = "Скачать"; link.className = "attachment-download"; @@ -75,7 +73,7 @@ document.addEventListener('DOMContentLoaded', () => { if (att.type && att.type.startsWith("image/")) { const img = document.createElement("img"); - img.src = att.dataUrl || att.url || ''; + img.src = att.dataUrl; img.alt = att.name; img.className = "attachment-preview"; el.appendChild(img); @@ -106,8 +104,7 @@ document.addEventListener('DOMContentLoaded', () => { } function emitAttachmentsUpdate() { - // По возможности отправляем только метаданные и URL файлов, чтобы не перегружать socket - const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, url: a.url })); + const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); } @@ -132,31 +129,7 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // После создания локальных превью — показываем их, затем загружаем файлы на сервер - Promise.all(readers).then(async () => { - renderAttachments(); - - try { - const form = new FormData(); - files.forEach((f) => form.append('files', f)); - const res = await fetch('/upload', { method: 'POST', body: form }); - if (res.ok) { - const data = await res.json(); - (data.files || []).forEach((u, idx) => { - const att = attachments[idx]; - if (att) { - att.url = u.url; - // удаляем dataUrl чтобы не отправлять большой base64 по sockets - delete att.dataUrl; - } - }); - } else { - console.warn('Upload failed', res.status); - } - } catch (err) { - console.error('Upload error', err); - } - + Promise.all(readers).then(() => { renderAttachments(); emitAttachmentsUpdate(); }); From 3ae9071cb4102bb2264c6dc880a6ab4c659fb23a Mon Sep 17 00:00:00 2001 From: falbue Date: Sat, 8 Nov 2025 19:01:33 +0500 Subject: [PATCH 6/9] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8,=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/files.css | 24 ++---------------------- static/scripts/files.js | 8 ++++++-- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/static/css/files.css b/static/css/files.css index 9021403..5e888af 100644 --- a/static/css/files.css +++ b/static/css/files.css @@ -1,4 +1,3 @@ -/* Стили для UI прикрепления файлов */ .attachments { display: flex; gap: 8px; @@ -12,16 +11,9 @@ align-items: center; gap: 8px; padding: 6px 8px; - background: rgba(0, 0, 0, 0.04); - border-radius: 8px; - border: 1px solid rgba(0, 0, 0, 0.06); min-width: 120px; } -.attachment.received { - background: rgba(0, 0, 0, 0.02); -} - .attachment-preview { width: 56px; height: 56px; @@ -47,26 +39,14 @@ } .attachment-remove { - margin-left: auto; - background: transparent; - border: none; - color: var(--text-color); font-size: 1.2rem; - cursor: pointer; } .textarea-wrapper.drag-over { - outline: 2px dashed rgba(0, 0, 0, 0.15); - border-radius: 8px; + outline: 2px dashed var(--text-color); + border-radius: 8px 8px 0 0; } .received-attachments { padding: 6px 12px; -} - -.attachment-download { - margin-left: 8px; - font-size: 0.9rem; - color: var(--text-color); - text-decoration: underline; } \ No newline at end of file diff --git a/static/scripts/files.js b/static/scripts/files.js index f65b45b..cfeb88b 100644 --- a/static/scripts/files.js +++ b/static/scripts/files.js @@ -48,8 +48,12 @@ document.addEventListener('DOMContentLoaded', () => { const link = document.createElement("a"); link.href = a.dataUrl; link.download = a.name; - link.textContent = "Скачать"; - link.className = "attachment-download"; + link.className = "button"; + + const icon = document.createElement("i"); + icon.className = "iconoir-download"; + link.appendChild(icon); + item.appendChild(link); container.appendChild(item); From efa328a3c7493104b28d53043c8c8a9a3787d3ce Mon Sep 17 00:00:00 2001 From: falbue Date: Tue, 11 Nov 2025 14:42:40 +0500 Subject: [PATCH 7/9] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83?= =?UTF-8?q?=D0=B5=D0=BC=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 23 +-- static/css/animation.css | 35 +--- static/css/files.css | 52 ------ static/css/main.css | 26 +-- static/css/variables.css | 14 +- static/scripts/calls.js | 334 --------------------------------------- static/scripts/files.js | 179 --------------------- templates/base.html | 1 - templates/chat.html | 26 --- 9 files changed, 25 insertions(+), 665 deletions(-) delete mode 100644 static/css/files.css delete mode 100644 static/scripts/calls.js delete mode 100644 static/scripts/files.js diff --git a/app.py b/app.py index acf41c0..70a77a2 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,4 @@ import uuid - from flask import Flask, render_template, request from flask_socketio import SocketIO, join_room @@ -25,7 +24,7 @@ def on_join(data): socketio.emit( "receive_message", {"text": "Пользователь подключился!", "sender_id": data["sender_id"]}, - to=chat_id, + room=chat_id, ) @@ -34,39 +33,31 @@ def handle_message(data): socketio.emit( "receive_message", {"text": data["text"], "sender_id": data["sender_id"]}, - to=data["chat_id"], + room=data["chat_id"], ) @socketio.on("call:request") def handle_call_request(data): chat_id = data["chatId"] - socketio.emit("call:incoming", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] + # Передаём ВЕСЬ объект data, включая sdp (offer) + socketio.emit("call:incoming", data, room=chat_id, skip_sid=request.sid) @socketio.on("call:response") def handle_call_response(data): chat_id = data["chatId"] if data["accepted"]: - socketio.emit("call:accepted", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] + socketio.emit("call:accepted", data, room=chat_id, skip_sid=request.sid) else: - socketio.emit("call:rejected", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] + socketio.emit("call:rejected", data, room=chat_id, skip_sid=request.sid) # WebRTC сигнализация @socketio.on("webrtc:ice-candidate") def handle_ice_candidate(data): chat_id = data["chatId"] - socketio.emit("webrtc:ice-candidate", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] - - -@socketio.on("update_attachments") -def handle_update_attachments(data): - """При получении списка прикреплённых файлов от клиента — рассылка остальным в комнате.""" - chat_id = data.get("chat_id") - if not chat_id: - return - socketio.emit("receive_attachments", data, to=chat_id, skip_sid=request.sid) # pyright: ignore[reportAttributeAccessIssue] + socketio.emit("webrtc:ice-candidate", data, room=chat_id, skip_sid=request.sid) if __name__ == "__main__": diff --git a/static/css/animation.css b/static/css/animation.css index cd76b11..da73e4f 100644 --- a/static/css/animation.css +++ b/static/css/animation.css @@ -5,52 +5,25 @@ button, box-shadow 300ms ease; } -button, +button:hover, .button:hover { transform: scale(1.01); box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.2); } -button, +button:active, .button:active { transform: scale(0.98); box-shadow: 0px 4px 8px rgba(0, 0, 0, 0); } -.call::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 12px; - padding: 2px; /* толщина обводки */ - background: linear-gradient(45deg, #ff8a00, #e52e71, #22c1c3); - -webkit-mask: - linear-gradient(#fff 0 0) content-box, - linear-gradient(#fff 0 0); - mask: - linear-gradient(#fff 0 0) content-box, - linear-gradient(#fff 0 0); - -webkit-mask-composite: destination-out; - mask-composite: exclude; - opacity: 1; - transition: opacity 0.3s ease; - pointer-events: none; -} - /* Опционально: анимация вращения градиента */ @keyframes rotate-border { 0% { background: linear-gradient(0deg, #ff8a00, #e52e71, #22c1c3); } + 100% { background: linear-gradient(360deg, #ff8a00, #e52e71, #22c1c3); } -} - -.call.animated-border::before { - animation: rotate-border 2s linear infinite; - opacity: 1; -} +} \ No newline at end of file diff --git a/static/css/files.css b/static/css/files.css deleted file mode 100644 index 5e888af..0000000 --- a/static/css/files.css +++ /dev/null @@ -1,52 +0,0 @@ -.attachments { - display: flex; - gap: 8px; - padding: 8px 12px; - flex-wrap: wrap; - box-sizing: border-box; -} - -.attachment { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 8px; - min-width: 120px; -} - -.attachment-preview { - width: 56px; - height: 56px; - object-fit: cover; - border-radius: 6px; -} - -.attachment-meta { - display: flex; - flex-direction: column; - font-size: 0.9rem; -} - -.attachment-name { - font-weight: 600; - color: var(--text-color); -} - -.attachment-size { - font-size: 0.8rem; - color: var(--text-color); - opacity: 0.7; -} - -.attachment-remove { - font-size: 1.2rem; -} - -.textarea-wrapper.drag-over { - outline: 2px dashed var(--text-color); - border-radius: 8px 8px 0 0; -} - -.received-attachments { - padding: 6px 12px; -} \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css index 81f7e73..8ecdaa3 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -16,6 +16,7 @@ body { padding: 0; background-color: var(--backgrond); } + main { height: var(--vh); box-sizing: border-box; @@ -31,9 +32,11 @@ a { h1 { font-size: 4rem; } + h2 { font-size: 2rem; } + h3 { font-size: 1.5rem; } @@ -42,9 +45,11 @@ h3 { h1 { font-size: clamp(2rem, 6vw, 3rem); } + h2 { font-size: clamp(1.125rem, 4vw, 1.125rem); } + h3 { font-size: clamp(1.125rem, 4.5vw, 1.25rem); } @@ -84,8 +89,8 @@ button, cursor: pointer; } -button > i, -.button > i { +button>i, +.button>i { font-size: 1.2rem; } @@ -119,6 +124,7 @@ button > i, padding: 20px 0; box-sizing: border-box; } + .hero::before { content: ""; flex: 0 0 auto; @@ -163,7 +169,7 @@ nav { flex-direction: row-reverse; } -nav > .buttons { +nav>.buttons { display: flex; align-items: center; justify-content: space-between; @@ -211,6 +217,7 @@ textarea { resize: none; overflow-y: auto; } + textarea:focus { outline: none; } @@ -221,9 +228,11 @@ textarea:focus { bottom: 20px; font-size: 1.4rem; } + nav { bottom: 100px !important; } + textarea { font-size: 1.2rem; } @@ -242,13 +251,4 @@ code { width: 100%; padding: 2px 6px; box-sizing: border-box; -} - -.accept { - background: var(--green); - color: var(--text-color); -} -.decline { - background: var(--red); - color: var(--text-color); -} +} \ No newline at end of file diff --git a/static/css/variables.css b/static/css/variables.css index db0f340..06c1b34 100644 --- a/static/css/variables.css +++ b/static/css/variables.css @@ -45,20 +45,8 @@ opacity: 0; pointer-events: none !important; } -.hidden * { - visibility: hidden !important; - width: 0; - height: 0; -} - -#videoCallButton { - display: none; -} -#videoContainer { - display: none; -} .show { visibility: visible !important; opacity: 1 !important; -} +} \ No newline at end of file diff --git a/static/scripts/calls.js b/static/scripts/calls.js deleted file mode 100644 index 82c9936..0000000 --- a/static/scripts/calls.js +++ /dev/null @@ -1,334 +0,0 @@ -const audioCallButton = document.getElementById("audioCallButton"); -const videoCallButton = document.getElementById("videoCallButton"); -const muteButton = document.getElementById("muteButton"); -const acceptCallBtn = document.getElementById("acceptCall"); -const declineCallBtn = document.getElementById("declineCall"); -const videoContainer = document.getElementById("videoContainer"); -const localVideo = document.getElementById("localVideo"); -const remoteVideo = document.getElementById("remoteVideo"); - -// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ СОСТОЯНИЯ -let localStream = null; -let remoteStream = null; -let peerConnection = null; -let isCaller = false; -let callType = "audio"; -let callTimeout = null; -let callId = null; -let pendingOffer = null; -let isVideoEnabled = false; -let isMuted = false; - -// КОНФИГУРАЦИЯ WEBRTC -const configuration = { - iceServers: [ - { - urls: "turn:turn.falbue.ru:3478", - username: "falbue", - credential: "turn_password", - }, - ], -}; - -function initPeerConnection(isInitiator) { - peerConnection = new RTCPeerConnection(configuration); - - peerConnection.onicecandidate = (event) => { - if (event.candidate) { - socket.emit("webrtc:ice-candidate", { - chatId, - senderId, - callId, - candidate: event.candidate, - }); - } - }; - - peerConnection.ontrack = (event) => { - const stream = event.streams[0]; - if (event.track.kind === "video") { - remoteVideo.srcObject = stream; - videoContainer.classList.remove("hidden"); - } else if (event.track.kind === "audio") { - remoteVideo.srcObject = stream; - } - }; - - if (localStream) { - localStream.getTracks().forEach((track) => { - peerConnection.addTrack(track, localStream); - }); - } -} - -// УПРАВЛЕНИЕ МЕДИАПОТОКОМ -async function getMedia(video = false) { - try { - const constraints = { - audio: true, - video: video ? { width: 320, height: 240 } : false, - }; - - const newStream = await navigator.mediaDevices.getUserMedia(constraints); - - if (localStream) { - localStream.getTracks().forEach((track) => track.stop()); - } - - localStream = newStream; - - if (localVideo) { - localVideo.srcObject = localStream; - } - - if (peerConnection) { - const localTracks = peerConnection.getSenders(); - localTracks.forEach((sender) => { - if (sender.track) { - if (sender.track.kind === "video" && newStream.getVideoTracks()[0]) { - sender.replaceTrack(newStream.getVideoTracks()[0]); - } - } - }); - } - - return true; - } catch (error) { - console.error("Ошибка получения медиа:", error); - notification("Не удалось получить доступ к микрофону/камере"); - return false; - } -} - -// УПРАВЛЕНИЕ ВИДИМОСТЬЮ КНОПОК -function hideAllButtons() { - audioCallButton.classList.add("hidden"); - videoCallButton.classList.add("hidden"); - muteButton.classList.add("hidden"); - acceptCallBtn.classList.add("hidden"); - declineCallBtn.classList.add("hidden"); -} - -function showButtons(buttons) { - hideAllButtons(); - buttons.forEach((btn) => btn.classList.remove("hidden")); -} - -// НАЧАЛО АУДИОЗВОНКА -async function startCall() { - if (!(await getMedia(false))) return; - - isCaller = true; - callType = "audio"; - callId = Date.now().toString(36) + Math.random().toString(36).substr(2); - - showButtons([videoCallButton, muteButton, declineCallBtn]); - initPeerConnection(true); - - try { - const offer = await peerConnection.createOffer(); - await peerConnection.setLocalDescription(offer); - - socket.emit("call:request", { - chatId, - senderId, - callId, - type: "audio", - sdp: offer, - }); - - callTimeout = setTimeout(() => { - if (isCaller) { - endCall(); - notification("Звонок не был принят"); - } - }, 30000); - } catch (error) { - console.error("Ошибка создания offer:", error); - endCall(); - } -} - -// ПЕРЕКЛЮЧЕНИЕ ВИДЕО (вкл/выкл) -async function toggleVideo() { - if (!isVideoEnabled) { - if (!(await getMedia(true))) return; - isVideoEnabled = true; - videoCallButton.classList.add("active"); - localVideo.classList.remove("hidden"); - videoContainer.classList.remove("hidden"); - } else { - if (localStream) { - const videoTrack = localStream.getVideoTracks()[0]; - if (videoTrack) { - videoTrack.stop(); - } - const videoSender = peerConnection - .getSenders() - .find((s) => s.track && s.track.kind === "video"); - if (videoSender) { - peerConnection.removeTrack(videoSender); - } - } - isVideoEnabled = false; - videoCallButton.classList.remove("active"); - localVideo.classList.add("hidden"); - if (!remoteVideo.srcObject) { - videoContainer.classList.add("hidden"); - } - } -} - -// ПЕРЕКЛЮЧЕНИЕ МИКРОФОНА (mute/unmute) -function toggleMute() { - if (!localStream) return; - - localStream.getAudioTracks().forEach((track) => { - track.enabled = !track.enabled; - }); - - isMuted = !isMuted; - muteButton.classList.toggle("muted", isMuted); - if (isMuted) { - muteButton.innerHTML = ''; - } else { - muteButton.innerHTML = ''; - } -} - -async function acceptCall() { - clearTimeout(callTimeout); - - if (!pendingOffer) { - console.error("Нет offer для ответа"); - declineCall(); - return; - } - - if (!(await getMedia(false))) { - declineCall(); - return; - } - - showButtons([videoCallButton, muteButton, declineCallBtn]); - initPeerConnection(false); - - try { - await peerConnection.setRemoteDescription(pendingOffer); - - const answer = await peerConnection.createAnswer(); - await peerConnection.setLocalDescription(answer); - - socket.emit("call:response", { - chatId, - senderId, - callId, - accepted: true, - sdp: answer, - }); - - pendingOffer = null; - } catch (error) { - console.error("Ошибка при принятии звонка:", error); - declineCall(); - } -} - -function declineCall() { - clearTimeout(callTimeout); - showButtons([audioCallButton]); - - socket.emit("call:response", { - chatId, - senderId, - callId, - accepted: false, - }); - - endCall(); -} - -function endCall() { - if (peerConnection) { - peerConnection.close(); - peerConnection = null; - } - - if (localStream) { - localStream.getTracks().forEach((track) => track.stop()); - localStream = null; - } - - if (remoteStream) { - remoteStream.getTracks().forEach((track) => track.stop()); - remoteStream = null; - } - - // localVideo.srcObject = null; - // remoteVideo.srcObject = null; - // videoContainer.classList.add('hidden'); - - showButtons([audioCallButton]); - - clearTimeout(callTimeout); - callId = null; - pendingOffer = null; - isCaller = false; - isVideoEnabled = false; - isMuted = false; -} - -// ОБРАБОТКА СОБЫТИЙ ПОЛЬЗОВАТЕЛЯ -audioCallButton?.addEventListener("click", startCall); -videoCallButton?.addEventListener("click", toggleVideo); -muteButton?.addEventListener("click", toggleMute); -acceptCallBtn?.addEventListener("click", acceptCall); -declineCallBtn?.addEventListener("click", declineCall); - -// ОБРАБОТКА СОКЕТ-СОБЫТИЙ -socket.on("call:incoming", (data) => { - if (data.senderId === senderId) return; - - callType = data.type; - callId = data.callId; - pendingOffer = data.sdp; - - showButtons([acceptCallBtn, declineCallBtn]); - notification("Поступил звонок"); - - callTimeout = setTimeout(() => { - declineCall(); - }, 30000); -}); - -socket.on("call:accepted", async (data) => { - if (data.callId !== callId) return; - - clearTimeout(callTimeout); - showButtons([videoCallButton, muteButton, declineCallBtn]); - - try { - await peerConnection.setRemoteDescription(data.sdp); - } catch (error) { - console.error("Ошибка установки ответа от собеседника:", error); - endCall(); - } -}); - -socket.on("call:rejected", (data) => { - if (data.callId !== callId) return; - - clearTimeout(callTimeout); - notification("Звонок отклонён"); - endCall(); -}); - -socket.on("webrtc:ice-candidate", async (data) => { - if (data.senderId === senderId || !peerConnection) return; - - try { - await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); - } catch (error) { - console.error("Ошибка добавления ICE кандидата:", error); - } -}); diff --git a/static/scripts/files.js b/static/scripts/files.js deleted file mode 100644 index cfeb88b..0000000 --- a/static/scripts/files.js +++ /dev/null @@ -1,179 +0,0 @@ -// Логика работы с прикреплениями (зависит от глобальных переменных: socket, chatId, senderId) -document.addEventListener('DOMContentLoaded', () => { - const attachButton = document.getElementById("attachButton"); - const fileInput = document.getElementById("fileInput"); - const attachmentsContainer = document.getElementById("attachments"); - - let attachments = []; // локальные прикрепления пользователя - - // Обработка входящих прикреплённых файлов от других пользователей - socket.on("receive_attachments", (data) => { - // ожидаем, что сервер пересылает { chat_id, sender_id, attachments: [...] } - const sender = data.sender_id; - - // Найдём уже отрисованный контейнер от этого отправителя (если был) - const chat = document.querySelector('.chat'); - if (!chat) return; - - const existing = chat.querySelector(`.received-attachments[data-sender="${sender}"]`); - - // Если нет прикреплений — удалить существующий контейнер (если есть) - if (!data.attachments || !data.attachments.length) { - if (existing) existing.remove(); - return; - } - - // Создадим новый контейнер - const container = document.createElement("div"); - container.className = "received-attachments"; - container.dataset.sender = sender; - - data.attachments.forEach((a) => { - const item = document.createElement("div"); - item.className = "attachment received"; - - if (a.type && a.type.startsWith("image/")) { - const img = document.createElement("img"); - img.src = a.dataUrl; - img.alt = a.name; - img.className = "attachment-preview"; - item.appendChild(img); - } - - const info = document.createElement("div"); - info.className = "attachment-info"; - info.innerHTML = `
${a.name}
${Math.round(a.size / 1024)} KB
`; - item.appendChild(info); - - const link = document.createElement("a"); - link.href = a.dataUrl; - link.download = a.name; - link.className = "button"; - - const icon = document.createElement("i"); - icon.className = "iconoir-download"; - link.appendChild(icon); - - item.appendChild(link); - - container.appendChild(item); - }); - - // Если уже есть — заменим, иначе добавим - if (existing) { - existing.replaceWith(container); - } else { - chat.appendChild(container); - } - }); - - function renderAttachments() { - if (!attachmentsContainer) return; - attachmentsContainer.innerHTML = ""; - attachments.forEach((att) => { - const el = document.createElement("div"); - el.className = "attachment"; - el.dataset.id = att.id; - - if (att.type && att.type.startsWith("image/")) { - const img = document.createElement("img"); - img.src = att.dataUrl; - img.alt = att.name; - img.className = "attachment-preview"; - el.appendChild(img); - } else { - const icon = document.createElement("i"); - icon.className = "iconoir-attachment"; - icon.style.fontSize = '1.1rem'; - el.appendChild(icon); - } - - const meta = document.createElement("div"); - meta.className = "attachment-meta"; - meta.innerHTML = `
${att.name}
${Math.round(att.size / 1024)} KB
`; - el.appendChild(meta); - - const removeBtn = document.createElement("button"); - removeBtn.type = 'button'; - removeBtn.className = 'attachment-remove'; - removeBtn.title = 'Удалить файл'; - removeBtn.textContent = '×'; - removeBtn.addEventListener('click', () => { - removeAttachment(att.id); - }); - el.appendChild(removeBtn); - - attachmentsContainer.appendChild(el); - }); - } - - function emitAttachmentsUpdate() { - const payload = attachments.map((a) => ({ id: a.id, name: a.name, size: a.size, type: a.type, dataUrl: a.dataUrl })); - socket.emit('update_attachments', { chat_id: chatId, sender_id: senderId, attachments: payload }); - } - - function removeAttachment(id) { - attachments = attachments.filter((a) => a.id !== id); - renderAttachments(); - emitAttachmentsUpdate(); - } - - function handleFiles(fileList) { - const files = Array.from(fileList); - const readers = files.map((file) => { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - const id = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); - const att = { id, name: file.name, size: file.size, type: file.type, dataUrl: e.target.result }; - attachments.push(att); - resolve(att); - }; - reader.readAsDataURL(file); - }); - }); - - Promise.all(readers).then(() => { - renderAttachments(); - emitAttachmentsUpdate(); - }); - } - - // Кнопка прикрепления - attachButton?.addEventListener('click', () => { - fileInput?.click(); - }); - - fileInput?.addEventListener('change', (e) => { - if (e.target.files && e.target.files.length) { - handleFiles(e.target.files); - e.target.value = null; - } - }); - - // Drag & drop на области ввода - const textareaWrapper = document.querySelector('.textarea-wrapper'); - if (textareaWrapper) { - ['dragenter', 'dragover'].forEach((ev) => { - textareaWrapper.addEventListener(ev, (e) => { - e.preventDefault(); - e.stopPropagation(); - textareaWrapper.classList.add('drag-over'); - }); - }); - ['dragleave', 'drop'].forEach((ev) => { - textareaWrapper.addEventListener(ev, (e) => { - e.preventDefault(); - e.stopPropagation(); - textareaWrapper.classList.remove('drag-over'); - }); - }); - - textareaWrapper.addEventListener('drop', (e) => { - const dt = e.dataTransfer; - if (dt && dt.files && dt.files.length) { - handleFiles(dt.files); - } - }); - } -}); diff --git a/templates/base.html b/templates/base.html index b898507..4fcdc46 100644 --- a/templates/base.html +++ b/templates/base.html @@ -33,7 +33,6 @@ - diff --git a/templates/chat.html b/templates/chat.html index d644a06..d12e2a9 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -8,37 +8,13 @@
-
-
@@ -46,8 +22,6 @@ - - From f98036adf1042fcba4768152661d8d6eb284ccf9 Mon Sep 17 00:00:00 2001 From: falbue Date: Tue, 11 Nov 2025 15:13:29 +0500 Subject: [PATCH 8/9] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=BE=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B=D0=B5=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B2=20=D1=81=D0=BE=D0=BA=D0=B5=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 44 ++++----------------- static/scripts/message.js | 82 +++------------------------------------ 2 files changed, 12 insertions(+), 114 deletions(-) diff --git a/app.py b/app.py index 70a77a2..7c5bb71 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ import uuid -from flask import Flask, render_template, request +from flask import Flask, render_template from flask_socketio import SocketIO, join_room app = Flask(__name__) @@ -17,48 +17,18 @@ def chat(chat_id): return render_template("chat.html", chat_id=chat_id) -@socketio.on("join_chat") -def on_join(data): - chat_id = data["chat_id"] - join_room(chat_id) - socketio.emit( - "receive_message", - {"text": "Пользователь подключился!", "sender_id": data["sender_id"]}, - room=chat_id, - ) - - @socketio.on("update_message") def handle_message(data): + chat_id = data.get("chat_id") + if chat_id: + join_room(chat_id) + socketio.emit( "receive_message", - {"text": data["text"], "sender_id": data["sender_id"]}, - room=data["chat_id"], + {"text": data.get("text"), "sender_id": data.get("sender_id")}, + to=chat_id, ) -@socketio.on("call:request") -def handle_call_request(data): - chat_id = data["chatId"] - # Передаём ВЕСЬ объект data, включая sdp (offer) - socketio.emit("call:incoming", data, room=chat_id, skip_sid=request.sid) - - -@socketio.on("call:response") -def handle_call_response(data): - chat_id = data["chatId"] - if data["accepted"]: - socketio.emit("call:accepted", data, room=chat_id, skip_sid=request.sid) - else: - socketio.emit("call:rejected", data, room=chat_id, skip_sid=request.sid) - - -# WebRTC сигнализация -@socketio.on("webrtc:ice-candidate") -def handle_ice_candidate(data): - chat_id = data["chatId"] - socketio.emit("webrtc:ice-candidate", data, room=chat_id, skip_sid=request.sid) - - if __name__ == "__main__": socketio.run(app, host="0.0.0.0", port=80, debug=True) diff --git a/static/scripts/message.js b/static/scripts/message.js index b2238f9..204522c 100644 --- a/static/scripts/message.js +++ b/static/scripts/message.js @@ -21,7 +21,11 @@ function formatMessage(message) { return escapedMessage.replace(/\n/g, "
"); } -socket.emit("join_chat", { chat_id: chatId, sender_id: senderId }); +socket.emit("update_message", { + chat_id: chatId, + text: "Пользователь подключился!", + sender_id: senderId, +}); inputMessage.addEventListener("input", () => { const messageText = inputMessage.value.trim() || "..."; @@ -38,82 +42,6 @@ socket.on("receive_message", (data) => { } }); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function typeText(elementId, text) { const element = document.querySelector(`#${elementId} b`); let i = 0; From 6e446082dd82f2b568be93187821d4692cf0d99f Mon Sep 17 00:00:00 2001 From: falbue Date: Tue, 11 Nov 2025 15:19:05 +0500 Subject: [PATCH 9/9] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83?= =?UTF-8?q?=D0=B5=D0=BC=D1=8B=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=86=D0=B2=D0=B5=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=88=D1=80=D0=B8=D1=84?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/main.css | 1 - static/css/variables.css | 2 -- templates/base.html | 6 ------ 3 files changed, 9 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index 8ecdaa3..857131c 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -100,7 +100,6 @@ button>i, inset: 20px auto auto 50%; transform: translateX(-50%); background-color: var(--button); - color: var(--adptive-text); padding: 10px 20px; border-radius: 12px; z-index: 9999; diff --git a/static/css/variables.css b/static/css/variables.css index 06c1b34..fe77489 100644 --- a/static/css/variables.css +++ b/static/css/variables.css @@ -13,7 +13,6 @@ @media (prefers-color-scheme: light) { :root { - --bg-color: var(--black); --text-color: var(--black); --button: var(--ligth2); --backgrond: var(--ligth); @@ -24,7 +23,6 @@ @media (prefers-color-scheme: dark) { :root { --text-color: var(--white); - --bg-color: var(--white); --button: var(--dark2); --backgrond: var(--dark); --input: var(--black); diff --git a/templates/base.html b/templates/base.html index 4fcdc46..cd80299 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,12 +17,6 @@ - - - -