521 lines
15 KiB
JavaScript
521 lines
15 KiB
JavaScript
// Function to delete message
|
||
async function deleteMessage(messageId, forEveryone) {
|
||
try {
|
||
const response = await fetch("/api/delete-message", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${currentToken}`,
|
||
},
|
||
body: JSON.stringify({
|
||
message_id: messageId,
|
||
for_everyone: forEveryone,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
console.log(`✅ Message deleted: ${data.message}`);
|
||
showAlert(data.message, "success");
|
||
|
||
// Remove message from UI
|
||
const messageElement = document.querySelector(
|
||
`[data-message-id="${messageId}"]`,
|
||
);
|
||
if (messageElement) {
|
||
messageElement.remove();
|
||
}
|
||
|
||
// Update chat history
|
||
if (messageHistory[currentRecipient]) {
|
||
messageHistory[currentRecipient] = messageHistory[
|
||
currentRecipient
|
||
].filter((msg) => msg.id !== messageId);
|
||
}
|
||
|
||
// Reload chats to update last message
|
||
loadChats();
|
||
} else {
|
||
console.error("❌ Failed to delete message:", data.message);
|
||
showAlert(data.message, "error");
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ Error deleting message:", error);
|
||
showAlert("Error deleting message", "error");
|
||
}
|
||
}
|
||
|
||
// Function to mark messages as read
|
||
async function markMessagesAsRead(fromUser) {
|
||
try {
|
||
const response = await fetch("/api/mark-read", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${currentToken}`,
|
||
},
|
||
body: JSON.stringify({
|
||
from_user: fromUser,
|
||
to_user: currentUser,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
console.log(`✅ Marked messages as read from ${fromUser}`);
|
||
// Обновляем список чатов чтобы убрать красный badge
|
||
loadChats();
|
||
} else {
|
||
console.error("❌ Failed to mark messages as read:", data.error);
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ Error marking messages as read:", error);
|
||
}
|
||
}
|
||
|
||
function deleteMessageForMe() {
|
||
if (messageToDelete) {
|
||
deleteMessage(messageToDelete, false);
|
||
closeDeleteModal();
|
||
}
|
||
}
|
||
|
||
function deleteMessageForEveryone() {
|
||
if (messageToDelete) {
|
||
deleteMessage(messageToDelete, true);
|
||
closeDeleteModal();
|
||
}
|
||
}
|
||
|
||
function handleChatMessage(msg) {
|
||
console.log("Handling chat message:", msg);
|
||
const otherUser = msg.from_user === currentUser ? msg.to_user : msg.from_user;
|
||
|
||
if (!messageHistory[otherUser]) {
|
||
messageHistory[otherUser] = [];
|
||
}
|
||
|
||
const messageExists = messageHistory[otherUser].some(
|
||
(existingMsg) => existingMsg.id === msg.id,
|
||
);
|
||
|
||
if (!messageExists) {
|
||
messageHistory[otherUser].push(msg);
|
||
}
|
||
|
||
loadChats();
|
||
|
||
if (otherUser === currentRecipient) {
|
||
displayMessage(msg, true);
|
||
// Если сообщение пришло в открытый чат, сразу помечаем как прочитанное
|
||
if (msg.from_user === currentRecipient) {
|
||
markMessagesAsRead(currentRecipient);
|
||
}
|
||
}
|
||
}
|
||
|
||
function displayMessage(msg, scrollToBottom) {
|
||
const chatMessages = document.getElementById("chatMessages");
|
||
|
||
// Remove empty state if it exists
|
||
const emptyState = chatMessages.querySelector(".empty-state");
|
||
if (emptyState) {
|
||
emptyState.remove();
|
||
}
|
||
|
||
const messageDiv = document.createElement("div");
|
||
messageDiv.className = `message ${msg.from_user === currentUser ? "outgoing" : "incoming"}`;
|
||
messageDiv.setAttribute("data-message-id", msg.id);
|
||
|
||
if (msg.message_type === "system") {
|
||
messageDiv.className = "system-message";
|
||
messageDiv.textContent = msg.content;
|
||
} else {
|
||
const isOutgoing = msg.from_user === currentUser;
|
||
|
||
if (!isOutgoing) {
|
||
const senderDiv = document.createElement("div");
|
||
senderDiv.className = "message-sender";
|
||
senderDiv.textContent = msg.from_user;
|
||
messageDiv.appendChild(senderDiv);
|
||
}
|
||
|
||
const contentDiv = document.createElement("div");
|
||
contentDiv.className = "message-content";
|
||
|
||
// Handle different message types
|
||
if (msg.message_type === "text") {
|
||
contentDiv.textContent = msg.content;
|
||
} else if (msg.message_type === "file") {
|
||
const fileInfo = JSON.parse(msg.content);
|
||
contentDiv.innerHTML = `
|
||
<div class="file-message">
|
||
<div class="file-icon">📄</div>
|
||
<div class="file-info">
|
||
<div class="file-name">${fileInfo.filename}</div>
|
||
<div class="file-size">${formatFileSize(fileInfo.size)}</div>
|
||
<div class="file-type">${fileInfo.mime_type}</div>
|
||
</div>
|
||
<a href="${fileInfo.filepath}" download="${fileInfo.filename}" class="download-btn">⬇️ Download</a>
|
||
</div>
|
||
`;
|
||
} else if (msg.message_type === "photo") {
|
||
const photoInfo = JSON.parse(msg.content);
|
||
contentDiv.innerHTML = `
|
||
<div class="media-message">
|
||
<img src="${photoInfo.filepath}" alt="Photo" class="message-image" onclick="viewImage('${photoInfo.filepath}')"/>
|
||
<div class="file-info">
|
||
<div class="file-name">${photoInfo.filename}</div>
|
||
<div class="file-size">${formatFileSize(photoInfo.size)}</div>
|
||
</div>
|
||
<a href="${photoInfo.filepath}" download="${photoInfo.filename}" class="download-btn">⬇️ Download</a>
|
||
</div>
|
||
`;
|
||
} else if (msg.message_type === "video") {
|
||
const videoInfo = JSON.parse(msg.content);
|
||
contentDiv.innerHTML = `
|
||
<div class="media-message">
|
||
<video controls class="message-video">
|
||
<source src="${videoInfo.filepath}" type="${videoInfo.mime_type}">
|
||
Your browser does not support the video tag.
|
||
</video>
|
||
<div class="file-info">
|
||
<div class="file-name">${videoInfo.filename}</div>
|
||
<div class="file-size">${formatFileSize(videoInfo.size)}</div>
|
||
</div>
|
||
<a href="${videoInfo.filepath}" download="${videoInfo.filename}" class="download-btn">⬇️ Download</a>
|
||
</div>
|
||
`;
|
||
} else if (msg.message_type === "audio") {
|
||
const audioInfo = JSON.parse(msg.content);
|
||
contentDiv.innerHTML = `
|
||
<div class="media-message">
|
||
<audio controls class="message-audio">
|
||
<source src="${audioInfo.filepath}" type="${audioInfo.mime_type}">
|
||
Your browser does not support the audio tag.
|
||
</audio>
|
||
<div class="file-info">
|
||
<div class="file-name">${audioInfo.filename}</div>
|
||
<div class="file-size">${formatFileSize(audioInfo.size)}</div>
|
||
</div>
|
||
<a href="${audioInfo.filepath}" download="${audioInfo.filename}" class="download-btn">⬇️ Download</a>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
messageDiv.appendChild(contentDiv);
|
||
|
||
const timeDiv = document.createElement("div");
|
||
timeDiv.className = "message-time";
|
||
const date = new Date(msg.timestamp);
|
||
timeDiv.textContent = date.toLocaleTimeString([], {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
messageDiv.appendChild(timeDiv);
|
||
|
||
// Add delete button for user's own messages or all messages
|
||
if (msg.from_user === currentUser) {
|
||
const actionsDiv = document.createElement("div");
|
||
actionsDiv.className = "message-actions";
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.className = "delete-btn";
|
||
deleteBtn.innerHTML = "×";
|
||
deleteBtn.title = "Delete message";
|
||
deleteBtn.onclick = (e) => {
|
||
console.log("delete message button clicked");
|
||
e.stopPropagation();
|
||
showDeleteModal(msg.id);
|
||
};
|
||
actionsDiv.appendChild(deleteBtn);
|
||
messageDiv.appendChild(actionsDiv);
|
||
}
|
||
}
|
||
|
||
chatMessages.appendChild(messageDiv);
|
||
console.log("message added");
|
||
|
||
if (scrollToBottom) {
|
||
setTimeout(() => {
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return "0 Bytes";
|
||
const k = 1024;
|
||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||
}
|
||
|
||
function viewImage(imageUrl) {
|
||
// Create a modal to view the image
|
||
const modal = document.createElement("div");
|
||
modal.className = "image-viewer-modal";
|
||
modal.innerHTML = `
|
||
<div class="image-viewer-content">
|
||
<span class="close-image-viewer">×</span>
|
||
<img src="${imageUrl}" alt="Full size image" class="image-viewer-large">
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// Close modal when clicking on close button
|
||
modal.querySelector(".close-image-viewer").onclick = function () {
|
||
document.body.removeChild(modal);
|
||
};
|
||
|
||
// Close modal when clicking outside the image
|
||
modal.onclick = function (event) {
|
||
if (event.target === modal) {
|
||
document.body.removeChild(modal);
|
||
}
|
||
};
|
||
}
|
||
|
||
function addSystemMessage(content) {
|
||
const chatMessages = document.getElementById("chatMessages");
|
||
|
||
// Remove empty state if it exists
|
||
const emptyState = chatMessages.querySelector(".empty-state");
|
||
if (emptyState) {
|
||
emptyState.remove();
|
||
}
|
||
|
||
const systemDiv = document.createElement("div");
|
||
systemDiv.className = "system-message";
|
||
systemDiv.textContent = content;
|
||
chatMessages.appendChild(systemDiv);
|
||
|
||
setTimeout(() => {
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
}, 100);
|
||
}
|
||
|
||
function sendMessage() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
showAlert("Please connect to the server first", "error");
|
||
return;
|
||
}
|
||
|
||
if (!currentRecipient) {
|
||
showAlert("Please select a user to chat with", "error");
|
||
return;
|
||
}
|
||
|
||
const input = document.getElementById("messageInput");
|
||
const content = input.value.trim();
|
||
|
||
if (!content) return;
|
||
|
||
const message = {
|
||
id: generateId(),
|
||
from_user: currentUser,
|
||
to_user: currentRecipient,
|
||
content: content,
|
||
timestamp: Date.now(),
|
||
message_type: "text",
|
||
};
|
||
|
||
console.log("Sending message:", message);
|
||
ws.send(JSON.stringify(message));
|
||
input.value = "";
|
||
input.style.height = "auto";
|
||
|
||
setTimeout(() => loadChats(), 100);
|
||
}
|
||
|
||
// Queue to handle file uploads sequentially
|
||
let uploadQueue = [];
|
||
let isUploading = false;
|
||
|
||
// Function to process the upload queue
|
||
async function processUploadQueue() {
|
||
if (isUploading || uploadQueue.length === 0) {
|
||
return;
|
||
}
|
||
|
||
isUploading = true;
|
||
const file = uploadQueue.shift(); // Get the first file in the queue
|
||
|
||
try {
|
||
await sendFileMessageInternal(file);
|
||
} catch (error) {
|
||
console.error("Upload failed for file:", error);
|
||
} finally {
|
||
isUploading = false;
|
||
// Process next file in queue if available
|
||
setTimeout(processUploadQueue, 100); // Small delay to allow server to settle
|
||
}
|
||
}
|
||
|
||
// Internal function to handle the actual file upload
|
||
async function sendFileMessageInternal(file) {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
showAlert("Please connect to the server first", "error");
|
||
return;
|
||
}
|
||
|
||
if (!currentRecipient) {
|
||
showAlert("Please select a user to chat with", "error");
|
||
return;
|
||
}
|
||
|
||
// Show upload progress
|
||
const chatMessages = document.getElementById("chatMessages");
|
||
const progressDiv = document.createElement("div");
|
||
progressDiv.className = "upload-progress";
|
||
progressDiv.innerHTML = `<div class="progress-text">Uploading ${file.name}</div><div class="progress-bar"><div class="progress-fill"></div></div>`;
|
||
chatMessages.appendChild(progressDiv);
|
||
|
||
try {
|
||
// Create FormData for file upload
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
|
||
// Upload file to server with better error handling
|
||
const response = await fetch("/api/upload", {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${currentToken}`,
|
||
},
|
||
body: formData,
|
||
}).catch((error) => {
|
||
console.error("Network error during upload:", error);
|
||
throw new Error("Network error during upload. Please try again.");
|
||
});
|
||
|
||
// Remove progress indicator
|
||
progressDiv.remove();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Upload failed: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.error || "Upload failed");
|
||
}
|
||
|
||
// Create message object based on file type
|
||
let message;
|
||
let messageType;
|
||
|
||
if (result.file_type === "photo") {
|
||
messageType = "photo";
|
||
message = {
|
||
id: generateId(),
|
||
from_user: currentUser,
|
||
to_user: currentRecipient,
|
||
content: JSON.stringify(result.info),
|
||
timestamp: Date.now(),
|
||
message_type: messageType,
|
||
};
|
||
} else if (result.file_type === "video") {
|
||
messageType = "video";
|
||
message = {
|
||
id: generateId(),
|
||
from_user: currentUser,
|
||
to_user: currentRecipient,
|
||
content: JSON.stringify(result.info),
|
||
timestamp: Date.now(),
|
||
message_type: messageType,
|
||
};
|
||
} else if (result.file_type === "audio") {
|
||
messageType = "audio";
|
||
message = {
|
||
id: generateId(),
|
||
from_user: currentUser,
|
||
to_user: currentRecipient,
|
||
content: JSON.stringify(result.info),
|
||
timestamp: Date.now(),
|
||
message_type: messageType,
|
||
};
|
||
} else {
|
||
messageType = "file";
|
||
message = {
|
||
id: generateId(),
|
||
from_user: currentUser,
|
||
to_user: currentRecipient,
|
||
content: JSON.stringify(result.info),
|
||
timestamp: Date.now(),
|
||
message_type: messageType,
|
||
};
|
||
}
|
||
|
||
console.log("Sending file message:", message);
|
||
ws.send(JSON.stringify(message));
|
||
|
||
setTimeout(() => loadChats(), 100);
|
||
} catch (error) {
|
||
// Remove progress indicator
|
||
progressDiv.remove();
|
||
console.error("Upload error:", error);
|
||
showAlert(`Failed to upload file: ${error.message}`, "error");
|
||
}
|
||
}
|
||
|
||
// Function to send a file message (adds to queue)
|
||
async function sendFileMessage(file) {
|
||
// Check if file is already in queue to prevent duplicates
|
||
const fileName = file.name;
|
||
const fileSize = file.size;
|
||
const isInQueue = uploadQueue.some(
|
||
(item) => item.name === fileName && item.size === fileSize,
|
||
);
|
||
|
||
if (isInQueue) {
|
||
console.log(
|
||
`File ${fileName} is already in upload queue, skipping duplicate.`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Add file to upload queue
|
||
uploadQueue.push(file);
|
||
console.log(
|
||
`File ${file.name} added to upload queue. Queue length: ${uploadQueue.length}`,
|
||
);
|
||
|
||
// Process the queue
|
||
setTimeout(processUploadQueue, 0);
|
||
}
|
||
|
||
// Event listeners
|
||
document
|
||
.getElementById("messageInput")
|
||
.addEventListener("keydown", function (e) {
|
||
if (e.key === "Enter" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
sendMessage();
|
||
}
|
||
});
|
||
|
||
// Track if event listener is already attached to prevent duplicates
|
||
if (!window.fileInputListenerAttached) {
|
||
// File input event listener
|
||
document.getElementById("fileInput").addEventListener("change", function (e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
// Add file to upload queue regardless of current upload status
|
||
// The queue system will handle the sequencing
|
||
uploadQueue.push(file);
|
||
console.log(
|
||
`File ${file.name} added to upload queue. Queue length: ${uploadQueue.length}`,
|
||
);
|
||
|
||
// Process the queue
|
||
setTimeout(processUploadQueue, 0);
|
||
|
||
// Reset the input so the same file can be selected again
|
||
e.target.value = "";
|
||
}
|
||
});
|
||
|
||
window.fileInputListenerAttached = true;
|
||
}
|