const messageIdentifierPrefix = `mintership-forum-message`; const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`; let replyToMessageIdentifier = null; let latestMessageIdentifiers = {}; // To keep track of the latest message in each room let currentPage = 0; // Track current pagination page let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once. // If there is a previous latest message identifiers, use them. Otherwise, use an empty. if (localStorage.getItem("latestMessageIdentifiers")) { latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers")); } document.addEventListener("DOMContentLoaded", async () => { // Identify the link for 'Mintership Forum' const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]'); mintershipForumLinks.forEach(link => { link.addEventListener('click', async (event) => { event.preventDefault(); await login(); // Assuming login is an async function await loadForumPage(); loadRoomContent("general"); // Automatically load General Room on forum load startPollingForNewMessages(); // Start polling for new messages after loading the forum page }); }); }); async function loadForumPage() { // Remove all sections except the menu const allSections = document.querySelectorAll('body > section'); allSections.forEach(section => { if (!section.classList.contains('menu')) { section.remove(); } }); // Check if user is an admin // const minterGroupAdmins = await fetchMinterGroupAdmins(); // const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin(); // Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');"> const mainContent = document.createElement('div'); mainContent.innerHTML = ` <div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;"> <div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;"> <div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div> </div> <div class="forum-submenu"> <div class="forum-rooms"> <button class="room-button" id="minters-room">Minters Room</button> ${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''} <button class="room-button" id="general-room">General Room</button> </div> </div> <div id="forum-content" class="forum-content"></div> </div> `; document.body.appendChild(mainContent); // Add event listeners to room buttons document.getElementById("minters-room").addEventListener("click", () => { currentPage = 0; loadRoomContent("minters"); }); if (userState.isAdmin) { document.getElementById("admins-room").addEventListener("click", () => { currentPage = 0; loadRoomContent("admins"); }); } document.getElementById("general-room").addEventListener("click", () => { currentPage = 0; loadRoomContent("general"); }); } function loadRoomContent(room) { const forumContent = document.getElementById("forum-content"); if (forumContent) { forumContent.innerHTML = ` <div class="room-content"> <h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3> <div id="messages-container" class="messages-container"></div> ${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''} <div class="message-input-section"> <div id="toolbar" class="message-toolbar"></div> <div id="editor" class="message-input"></div> <div class="attachment-section"> <input type="file" id="file-input" class="file-input" multiple> <button id="attach-button" class="attach-button">Attach Files</button> </div> <button id="send-button" class="send-button">Send</button> </div> </div> `; // Initialize Quill editor for rich text input const quill = new Quill('#editor', { theme: 'snow', modules: { toolbar: [ [{ 'font': [] }], // Add font family options [{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options [{ 'header': [1, 2, false] }], ['bold', 'italic', 'underline'], // Text formatting options [{ 'list': 'ordered'}, { 'list': 'bullet' }], ['link', 'blockquote', 'code-block'], [{ 'color': [] }, { 'background': [] }], // Text color and background color options [{ 'align': [] }], // Text alignment ['clean'] // Remove formatting button ] } }); // Load messages from QDN for the selected room loadMessagesFromQDN(room, currentPage); let selectedFiles = []; // Add event listener to handle file selection document.getElementById('file-input').addEventListener('change', (event) => { selectedFiles = Array.from(event.target.files); }); // Add event listener for the send button document.getElementById("send-button").addEventListener("click", async () => { const messageHtml = quill.root.innerHTML.trim(); if (messageHtml !== "" || selectedFiles.length > 0) { const randomID = await uid(); const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`; let attachmentIdentifiers = []; // Handle attachments for (const file of selectedFiles) { const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`; try { await qortalRequest({ action: "PUBLISH_QDN_RESOURCE", name: userState.accountName, service: "FILE", identifier: attachmentID, file: file, filename: file.name, filetype: file.type, }); attachmentIdentifiers.push({ identifier: attachmentID, filename: file.name, mimeType: file.type }); console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`); } catch (error) { console.error(`Error publishing attachment ${file.name}:`, error); } } // Create message object with unique identifier, HTML content, and attachments const messageObject = { messageHtml: messageHtml, hasAttachment: attachmentIdentifiers.length > 0, attachments: attachmentIdentifiers, replyTo: replyToMessageIdentifier }; try { // Convert message object to base64 let base64Message = await objectToBase64(messageObject); if (!base64Message) { console.log(`initial object creation with object failed, using btoa...`); base64Message = btoa(JSON.stringify(messageObject)); } // Publish message to QDN await qortalRequest({ action: "PUBLISH_QDN_RESOURCE", name: userState.accountName, service: "BLOG_POST", identifier: messageIdentifier, data64: base64Message }); console.log("Message published successfully"); // Clear the editor after sending the message, including any potential attached files and replies. quill.root.innerHTML = ""; document.getElementById('file-input').value = ""; selectedFiles = []; replyToMessageIdentifier = null; const replyContainer = document.querySelector(".reply-container"); if (replyContainer) { replyContainer.remove() } // Update the latest message identifier - DO NOT DO THIS ON PUBLISH, OR MESSAGE WILL NOT BE LOADED CORRECTLY. // latestMessageIdentifiers[room] = messageIdentifier; // localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers)); // Show success notification const notification = document.createElement('div'); notification.innerText = "Message published successfully! Message will take a confirmation to show."; notification.style.color = "green"; notification.style.marginTop = "10px"; document.querySelector(".message-input-section").appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } catch (error) { console.error("Error publishing message:", error); } } }); // Add event listener for the load more button document.getElementById("load-more-button").addEventListener("click", () => { currentPage++; loadMessagesFromQDN(room, currentPage); }); } } // Load messages for any given room with pagination async function loadMessagesFromQDN(room, page, isPolling = false) { try { // const offset = page * 10; const offset = 0; const limit = 0; // Get the set of existing identifiers from the messages container const messagesContainer = document.querySelector("#messages-container"); existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier)); // Fetch only messages that are not already present in the messages container const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers); if (messagesContainer) { // If there are no messages and we're not polling, display "no messages" message if (!response || !response.length) { if (page === 0 && !isPolling) { messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`; } return; } // Define `mostRecentMessage` to track the latest message during this fetch let mostRecentMessage = null; // Fetch all messages that haven't been fetched before const fetchMessages = await Promise.all(response.map(async (resource) => { try { console.log(`Fetching message with identifier: ${resource.identifier}`); const messageResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: resource.name, service: "BLOG_POST", identifier: resource.identifier, }); console.log("Fetched message response:", messageResponse); // No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set. const messageObject = messageResponse; const timestamp = resource.updated || resource.created; const formattedTimestamp = await timestampToHumanReadableDate(timestamp); return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo, timestamp, attachments: messageObject.attachments || [] // Include attachments if they exist }; } catch (error) { console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`); return null; } })); // Render new messages without duplication for (const message of fetchMessages) { if (message && !existingIdentifiers.has(message.identifier)) { let replyHtml = ""; if (message.replyTo) { const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo); if (repliedMessage) { replyHtml = ` <div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;"> <div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div> <div class="reply-content">${repliedMessage.content}</div> </div> `; } } const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp); let attachmentHtml = ""; if (message.attachments && message.attachments.length > 0) { for (const attachment of message.attachments) { if (attachment.mimeType.startsWith('image/')) { try { // Fetch the base64 string for the image const image = await fetchFileBase64(attachment.service, attachment.name, attachment.identifier); // Create a data URL for the Base64 string const dataUrl = `data:${attachment.mimeType};base64,${image}`; // Add the image HTML with the data URL attachmentHtml += `<div class="attachment"><img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"></div>`; } catch (error) { console.error(`Failed to fetch attachment ${attachment.filename}:`, error); } } else { // Display a button to download other attachments attachmentHtml += `<div class="attachment"> <button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button> </div>`; } } } const messageHTML = ` <div class="message-item" data-identifier="${message.identifier}"> ${replyHtml} <div class="message-header"> <span class="username">${message.name}</span> <span class="timestamp">${message.date}</span> ${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''} </div> ${attachmentHtml} <div class="message-text">${message.content}</div> <button class="reply-button" data-message-identifier="${message.identifier}">Reply</button> </div> `; // Append new message to the end of the container messagesContainer.insertAdjacentHTML('beforeend', messageHTML); // Track the most recent message if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) { mostRecentMessage = message; } } } // Update latestMessageIdentifiers for the room if (mostRecentMessage) { latestMessageIdentifiers[room] = { latestIdentifier: mostRecentMessage.identifier, latestTimestamp: mostRecentMessage.timestamp }; localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers)); } // Add event listeners to the reply buttons const replyButtons = document.querySelectorAll(".reply-button"); replyButtons.forEach(button => { button.addEventListener("click", () => { replyToMessageIdentifier = button.dataset.messageIdentifier; // Find the message being replied to const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier); if (repliedMessage) { const replyContainer = document.createElement("div"); replyContainer.className = "reply-container"; replyContainer.innerHTML = ` <div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;"> <strong>Replying to:</strong> ${repliedMessage.content} <button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button> </div> `; if (!document.querySelector(".reply-container")) { const messageInputSection = document.querySelector(".message-input-section"); if (messageInputSection) { messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild); // Add a listener for the cancel reply button document.getElementById("cancel-reply").addEventListener("click", () => { replyToMessageIdentifier = null; replyContainer.remove(); }); } } const messageInputSection = document.querySelector(".message-input-section"); const editor = document.querySelector(".ql-editor"); if (messageInputSection) { messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); } if (editor) { editor.focus(); } } }); }); } } catch (error) { console.error('Error loading messages from QDN:', error); } } // Polling function to check for new messages without clearing existing ones function startPollingForNewMessages() { setInterval(async () => { const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0]; if (activeRoom) { await loadMessagesFromQDN(activeRoom, currentPage, true); } }, 20000); }