diff --git a/.sync_b2a788d19481.db b/.sync_b2a788d19481.db index 447bc37..258c51a 100644 Binary files a/.sync_b2a788d19481.db and b/.sync_b2a788d19481.db differ diff --git a/.sync_b2a788d19481.db-shm b/.sync_b2a788d19481.db-shm index 69fd27b..5fe02fd 100644 Binary files a/.sync_b2a788d19481.db-shm and b/.sync_b2a788d19481.db-shm differ diff --git a/.sync_b2a788d19481.db-wal b/.sync_b2a788d19481.db-wal index 467da76..30a7b88 100644 Binary files a/.sync_b2a788d19481.db-wal and b/.sync_b2a788d19481.db-wal differ diff --git a/assets/css/forum-styles.css b/assets/css/forum-styles.css index 1efc816..5bab08a 100644 --- a/assets/css/forum-styles.css +++ b/assets/css/forum-styles.css @@ -1011,7 +1011,7 @@ body { .comments-container { margin-bottom: 10px; - max-height: 150px; + max-height: 300px; overflow-y: auto; } diff --git a/assets/js/AdminBoard.js b/assets/js/AdminBoard.js index f94af72..a466762 100644 --- a/assets/js/AdminBoard.js +++ b/assets/js/AdminBoard.js @@ -1,9 +1,9 @@ // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. -const isEncryptedTestMode = true; -const encryptedCardIdentifierPrefix = "test-MDC"; -let isExistingEncryptedCard = false; -let existingDecryptedCardData = {}; -let existingEncryptedCardIdentifier = {}; +const isEncryptedTestMode = true +const encryptedCardIdentifierPrefix = "test-MDC" +let isExistingEncryptedCard = false +let existingDecryptedCardData = {} +let existingEncryptedCardIdentifier = {} let cardMinterName = {} let existingCardMinterNames = [] @@ -93,7 +93,7 @@ const loadAdminBoardPage = async () => { await publishEncryptedCard(); }); - await createCardMinterNameList(); + // await fetchAndValidateAllAdminCards(); await fetchAllEncryptedCards(); } @@ -102,55 +102,75 @@ const extractCardsMinterName = (cardIdentifier) => { if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) { throw new Error('Invalid identifier format or prefix mismatch'); } - // Split the identifier into parts const parts = cardIdentifier.split('-'); - // Ensure the format has at least 3 parts if (parts.length < 3) { throw new Error('Invalid identifier format'); } - // Extract minterName (everything from the second part to the second-to-last part) const minterName = parts.slice(2, -1).join('-'); - // Return the extracted minterName return minterName; } +const processCards = async (validEncryptedCards) => { + const latestCardsMap = new Map() -const createCardMinterNameList = async () => { - - const response = await qortalRequest({ - action: "SEARCH_QDN_RESOURCES", - service: "MAIL_PRIVATE", - query: `${encryptedCardIdentifierPrefix}`, - mode: "ALL", - }); + // Step 1: Filter and keep the most recent card per identifier + validEncryptedCards.forEach(card => { + const timestamp = card.updated || card.created || 0 + const existingCard = latestCardsMap.get(card.identifier) - const validatedEncryptedCards = await Promise.all( - response.map(async card => { - const isValid = await validateEncryptedCardIdentifier(card); - return isValid ? card : null; - }) - ) + if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) { + latestCardsMap.set(card.identifier, card) + } + }) - const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null); + // Step 2: Extract unique cards + const uniqueValidCards = Array.from(latestCardsMap.values()) - if (validEncryptedCards.length === 0) { - console.log(`no matches found, not adding any names to name list.`) - return; - } + // Step 3: Group by minterName and select the most recent card per minterName + const minterNameMap = new Map() - for (const result of validEncryptedCards) { - const minterName = await extractCardsMinterName(result.identifier) + for (const card of validEncryptedCards) { + const minterName = await extractCardsMinterName(card.identifier) + const existingCard = minterNameMap.get(minterName) + const cardTimestamp = card.updated || card.created || 0 + const existingTimestamp = existingCard?.updated || existingCard?.created || 0 if (!existingCardMinterNames.includes(minterName)) { existingCardMinterNames.push(minterName) console.log(`cardsMinterName: ${minterName} - added to list`) } + + // Keep only the most recent card for each minterName + if (!existingCard || cardTimestamp > existingTimestamp) { + minterNameMap.set(minterName, card) + } } -}; + + // Step 4: Filter cards to ensure each minterName is included only once + const finalCards = [] + const seenMinterNames = new Set() + + for (const [minterName, card] of minterNameMap.entries()) { + if (!seenMinterNames.has(minterName)) { + finalCards.push(card) + seenMinterNames.add(minterName) // Mark the minterName as seen + } + } + + // Step 5: Sort by the most recent timestamp + finalCards.sort((a, b) => { + const timestampA = a.updated || a.created || 0 + const timestampB = b.updated || b.created || 0 + return timestampB - timestampA + }) + + return finalCards +} + //Main function to load the Minter Cards ---------------------------------------- const fetchAllEncryptedCards = async () => { @@ -179,40 +199,22 @@ const fetchAllEncryptedCards = async () => { ); const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null); - + if (validEncryptedCards.length === 0) { encryptedCardsContainer.innerHTML = "

No valid cards found.

"; return; } - - // Group by identifier and keep only the newest card for each identifier - const latestCardsMap = new Map(); - - validEncryptedCards.forEach(card => { - const timestamp = card.updated || card.created || 0; - const existingCard = latestCardsMap.get(card.identifier); - - if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) { - latestCardsMap.set(card.identifier, card); - } - }); - - // Extract unique cards and sort by timestamp descending - const uniqueValidCards = Array.from(latestCardsMap.values()).sort((a, b) => { - const timestampA = a.updated || a.created || 0; - const timestampB = b.updated || b.created || 0; - return timestampB - timestampA; - }); + const finalCards = await processCards(validEncryptedCards) // Display skeleton cards immediately encryptedCardsContainer.innerHTML = ""; - uniqueValidCards.forEach(card => { + finalCards.forEach(card => { const skeletonHTML = createSkeletonCardHTML(card.identifier); encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML); }); // Fetch and update each card - uniqueValidCards.forEach(async card => { + finalCards.forEach(async card => { try { const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", @@ -240,9 +242,9 @@ const fetchAllEncryptedCards = async () => { // Fetch poll results const pollResults = await fetchPollResults(decryptedCardData.poll); const minterNameFromIdentifier = await extractCardsMinterName(card.identifier); - + const commentCount = await getCommentCount(card.identifier); // Generate final card HTML - const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier); + const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, commentCount); replaceEncryptedSkeleton(card.identifier, finalCardHTML); } catch (error) { console.error(`Error processing card ${card.identifier}:`, error); @@ -489,6 +491,22 @@ const publishEncryptedCard = async () => { } } +const getCommentCount = async (cardIdentifier) => { + try { + const response = await qortalRequest({ + action: 'SEARCH_QDN_RESOURCES', + service: 'MAIL_PRIVATE', + query: `comment-${cardIdentifier}`, + mode: "ALL" + }); + // Just return the count; no need to decrypt each comment here + return Array.isArray(response) ? response.length : 0; + } catch (error) { + console.error(`Error fetching comment count for ${cardIdentifier}:`, error); + return 0; + } +}; + // Post a comment on a card. --------------------------------- const postEncryptedComment = async (cardIdentifier) => { const commentInput = document.getElementById(`new-comment-${cardIdentifier}`); @@ -498,7 +516,7 @@ const postEncryptedComment = async (cardIdentifier) => { return; } - const postTimestamp = `${Date.now()}` + const postTimestamp = Date.now() console.log(`timestmp to be posted: ${postTimestamp}`) const commentData = { @@ -575,7 +593,8 @@ const displayEncryptedComments = async (cardIdentifier) => { const decryptedCommentData = await decryptAndParseObject(commentDataResponse) - const timestamp = await timestampToHumanReadableDate(decryptedCommentData.timestamp); + const timestampCheck = comment.updated || comment.created || 0 + const timestamp = await timestampToHumanReadableDate(timestampCheck); //TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section. const commentHTML = ` @@ -688,7 +707,7 @@ const processQortalLinkForRendering = async (link) => { } // Create the overall Minter Card HTML ----------------------------------------------- -const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier) => { +const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const { minterName, header, content, links, creator, timestamp, poll } = cardData; const formattedDate = new Date(timestamp).toLocaleString(); const minterAvatar = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`; @@ -740,7 +759,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier) =>
- +
diff --git a/assets/js/Q-Mintership.js b/assets/js/Q-Mintership.js index 3ea3224..4da6a66 100644 --- a/assets/js/Q-Mintership.js +++ b/assets/js/Q-Mintership.js @@ -554,281 +554,330 @@ const generateAttachmentID = (room, fileIndex = null) => { return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID; }; -const decryptFile = async (encryptedData) => { - const publicKey = await getPublicKeyByName(userState.accountName) - const response = await qortalRequest({ - action: 'DECRYPT_DATA', - encryptedData, // has to be in base64 format - // publicKey: publicKey // requires the public key of the opposite user with whom you've created the encrypted data. - }); - const decryptedObject = response - return decryptedObject -} +// const decryptFile = async (encryptedData) => { +// const publicKey = await getPublicKeyByName(userState.accountName) +// const response = await qortalRequest({ +// action: 'DECRYPT_DATA', +// encryptedData, // has to be in base64 format +// // publicKey: publicKey // requires the public key of the opposite user with whom you've created the encrypted data. +// }); +// const decryptedObject = response +// return decryptedObject +// } +// --- REFACTORED LOAD MESSAGES AND HELPER FUNCTIONS --- const loadMessagesFromQDN = async (room, page, isPolling = false) => { try { const limit = 10; const offset = page * limit; - console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`); + console.log(`Loading messages from QDN: room=${room}, page=${page}, offset=${offset}, limit=${limit}`); - // Get the messages container const messagesContainer = document.querySelector("#messages-container"); if (!messagesContainer) return; - // If not polling, clear the message container and the existing identifiers for a fresh load - if (!isPolling) { - messagesContainer.innerHTML = ""; // Clear the messages container before loading new page - existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load - } + prepareMessageContainer(messagesContainer, isPolling); - // Get the set of existing identifiers from the messages container - existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier)); + const { service, query } = getServiceAndQuery(room); + const response = await fetchResourceList(service, query, limit, offset, room); - // Fetch messages for the current room and page - const service = room === "admins" ? "MAIL_PRIVATE" : "BLOG_POST" - const query = room === "admins" ? `${messageIdentifierPrefix}-${room}-e` : `${messageIdentifierPrefix}-${room}` - - const response = await searchAllWithOffset(service, query, limit, offset, room); - console.log(`Fetched messages count: ${response.length} for page: ${page}`); + console.log(`Fetched ${response.length} message(s) for page ${page}.`); - if (response.length === 0) { - // If no messages are fetched and it's not polling, display "no messages" for the initial load - if (page === 0 && !isPolling) { - messagesContainer.innerHTML = `

No messages found. Be the first to post!

`; - } + if (handleNoMessagesScenario(isPolling, page, response, messagesContainer)) { return; } - // Define `mostRecentMessage` to track the latest message during this fetch - let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null; - let firstNewMessageIdentifier = null - - // Fetch all messages that haven't been fetched before - const fetchMessages = await Promise.all(response.map(async (resource) => { - if (existingIdentifiers.has(resource.identifier)) { - return null; // Skip messages that are already displayed - } - - try { - console.log(`Fetching message with identifier: ${resource.identifier}`); - const messageResponse = await qortalRequest({ - action: "FETCH_QDN_RESOURCE", - name: resource.name, - service, - identifier: resource.identifier, - ...(room === "admins" ? { encoding: "base64" } : {}), - }); - - console.log("Fetched message response:", messageResponse); - - const timestamp = resource.updated || resource.created; - const formattedTimestamp = await timestampToHumanReadableDate(timestamp); - - let messageObject; - - if (room === "admins") { - try { - const decryptedData = await decryptObject(messageResponse); - messageObject = JSON.parse(atob(decryptedData)) - } catch (error) { - console.error(`Failed to decrypt message: ${error.message}`); - return { - name: resource.name, - content: "Encrypted message cannot be displayed", - date: formattedTimestamp, - identifier: resource.identifier, - replyTo: null, - timestamp, - attachments: [], - }; - } - } else { - messageObject = messageResponse; - } - - return { - name: resource.name, - content: messageObject?.messageHtml || "Message content missing", - date: formattedTimestamp, - identifier: resource.identifier, - replyTo: messageObject?.replyTo || null, - timestamp, - attachments: messageObject?.attachments || [], - }; - } catch (error) { - console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`); - return { - name: resource.name, - content: "Error loading message", - date: "Unknown", - identifier: resource.identifier, - replyTo: null, - timestamp: resource.updated || resource.created, - attachments: [], - }; - } - }) + // Re-establish existing identifiers after preparing container + existingIdentifiers = new Set( + Array.from(messagesContainer.querySelectorAll('.message-item')) + .map(item => item.dataset.identifier) ); - // Render new messages without duplication - for (const message of fetchMessages) { - if (message && !existingIdentifiers.has(message.identifier)) { - const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp); - if (isNewMessage && !firstNewMessageIdentifier) { - firstNewMessageIdentifier = message.identifier; - } - let replyHtml = ""; - if (message.replyTo) { - const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo); - if (repliedMessage) { - replyHtml = ` -
-
In reply to: ${repliedMessage.name} ${repliedMessage.date}
-
${repliedMessage.content}
-
- `; - } - } + let mostRecentMessage = getCurrentMostRecentMessage(room); - let attachmentHtml = ""; - if (message.attachments && message.attachments.length > 0) { - for (const attachment of message.attachments) { - if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) { - try { - // Construct the image URL - const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`; - - // Add the image HTML with the direct URL - attachmentHtml += `
- ${attachment.filename} -
`; - - // Set up the modal download button - const downloadButton = document.getElementById("download-button"); - downloadButton.onclick = () => { - fetchAndSaveAttachment( - attachment.service, - attachment.name, - attachment.identifier, - attachment.filename, - attachment.mimeType - ); - }; - } catch (error) { - console.error(`Failed to fetch attachment ${attachment.filename}:`, error); - } - } else { - // Display a button to download non-image attachments - attachmentHtml += `
- -
`; - } - } - } - - const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`; - const messageHTML = ` -
-
-
- Avatar - ${message.name} - ${isNewMessage ? `NEW` : ''} -
- ${message.date} -
- ${replyHtml} -
${message.content}
- - -
- `; - - // Append new message to the end of the container - messagesContainer.insertAdjacentHTML('beforeend', messageHTML); - - // Update mostRecentMessage if this message is newer - if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) { - mostRecentMessage = { - latestIdentifier: message.identifier, - latestTimestamp: message.timestamp - }; - } - - // Add the identifier to the existingIdentifiers set - existingIdentifiers.add(message.identifier); - } - } + const fetchMessages = await fetchAllMessages(response, service, room); + const { firstNewMessageIdentifier, updatedMostRecentMessage } = renderNewMessages( + fetchMessages, + existingIdentifiers, + messagesContainer, + room, + mostRecentMessage + ); if (firstNewMessageIdentifier && !isPolling) { - // Scroll to the first new message - const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`); - if (newMessageElement) { - newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } + scrollToNewMessages(firstNewMessageIdentifier); } - // Update latestMessageIdentifiers for the room - if (mostRecentMessage) { - latestMessageIdentifiers[room] = mostRecentMessage; - localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers)); + if (updatedMostRecentMessage) { + updateLatestMessageIdentifiers(room, updatedMostRecentMessage); } - // 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); + handleReplyLogic(fetchMessages); - if (repliedMessage) { - const replyContainer = document.createElement("div"); - replyContainer.className = "reply-container"; - replyContainer.innerHTML = ` -
- Replying to: ${repliedMessage.content} - -
- `; - - 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(); - } - } - }); - }); - - // Render pagination controls - const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`); - renderPaginationControls(room, totalMessages, limit); + await updatePaginationControls(room, limit); } catch (error) { console.error('Error loading messages from QDN:', error); } -} +}; + +/** Helper Functions (Arrow Functions) **/ + +const prepareMessageContainer = (messagesContainer, isPolling) => { + if (!isPolling) { + messagesContainer.innerHTML = ""; + existingIdentifiers.clear(); + } +}; + +const getServiceAndQuery = (room) => { + const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"; + const query = (room === "admins") + ? `${messageIdentifierPrefix}-${room}-e` + : `${messageIdentifierPrefix}-${room}`; + return { service, query }; +}; + +const fetchResourceList = async (service, query, limit, offset, room) => { + return await searchAllWithOffset(service, query, limit, offset, room); +}; + +const handleNoMessagesScenario = (isPolling, page, response, messagesContainer) => { + if (response.length === 0) { + if (page === 0 && !isPolling) { + messagesContainer.innerHTML = `

No messages found. Be the first to post!

`; + } + return true; + } + return false; +}; + +const getCurrentMostRecentMessage = (room) => { + return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null; +}; + +const fetchAllMessages = async (response, service, room) => { + return Promise.all(response.map(resource => fetchFullMessage(resource, service, room))); +}; + +const fetchFullMessage = async (resource, service, room) => { + try { + // Skip if already displayed + if (existingIdentifiers.has(resource.identifier)) { + return null; + } + + console.log(`Fetching message with identifier: ${resource.identifier}`); + const messageResponse = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: resource.name, + service, + identifier: resource.identifier, + ...(room === "admins" ? { encoding: "base64" } : {}), + }); + + const timestamp = resource.updated || resource.created; + const formattedTimestamp = await timestampToHumanReadableDate(timestamp); + const messageObject = await processMessageObject(messageResponse, room); + + return { + name: resource.name, + content: messageObject?.messageHtml || "Message content missing", + date: formattedTimestamp, + identifier: resource.identifier, + replyTo: messageObject?.replyTo || null, + timestamp, + attachments: messageObject?.attachments || [], + }; + } catch (error) { + console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`); + return { + name: resource.name, + content: "Error loading message", + date: "Unknown", + identifier: resource.identifier, + replyTo: null, + timestamp: resource.updated || resource.created, + attachments: [], + }; + } +}; + +const processMessageObject = async (messageResponse, room) => { + if (room !== "admins") { + return messageResponse; + } + + try { + const decryptedData = await decryptAndParseObject(messageResponse); + return decryptedData + } catch (error) { + console.error(`Failed to decrypt admin message: ${error.message}`); + return null; + } +}; + +const renderNewMessages = (fetchMessages, existingIdentifiers, messagesContainer, room, mostRecentMessage) => { + let firstNewMessageIdentifier = null; + let updatedMostRecentMessage = mostRecentMessage; + + for (const message of fetchMessages) { + if (message && !existingIdentifiers.has(message.identifier)) { + const isNewMessage = isMessageNew(message, mostRecentMessage); + if (isNewMessage && !firstNewMessageIdentifier) { + firstNewMessageIdentifier = message.identifier; + } + + const messageHTML = buildMessageHTML(message, fetchMessages, room, isNewMessage); + messagesContainer.insertAdjacentHTML('beforeend', messageHTML); + + if (!updatedMostRecentMessage || new Date(message.timestamp) > new Date(updatedMostRecentMessage?.latestTimestamp || 0)) { + updatedMostRecentMessage = { + latestIdentifier: message.identifier, + latestTimestamp: message.timestamp, + }; + } + + existingIdentifiers.add(message.identifier); + } + } + + return { firstNewMessageIdentifier, updatedMostRecentMessage }; +}; + +const isMessageNew = (message, mostRecentMessage) => { + return !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp); +}; + +const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => { + const replyHtml = buildReplyHtml(message, fetchMessages); + const attachmentHtml = buildAttachmentHtml(message, room); + const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`; + + return ` +
+
+
+ Avatar + ${message.name} + ${isNewMessage ? `NEW` : ''} +
+ ${message.date} +
+ ${replyHtml} +
${message.content}
+ + +
+ `; +}; + +const buildReplyHtml = (message, fetchMessages) => { + if (!message.replyTo) return ""; + + const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo); + if (!repliedMessage) return ""; + + return ` +
+
In reply to: ${repliedMessage.name} ${repliedMessage.date}
+
${repliedMessage.content}
+
+ `; +}; + +const buildAttachmentHtml = (message, room) => { + if (!message.attachments || message.attachments.length === 0) return ""; + + return message.attachments.map(attachment => buildSingleAttachmentHtml(attachment, room)).join(""); +}; + +const buildSingleAttachmentHtml = (attachment, room) => { + if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) { + const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`; + return ` +
+ ${attachment.filename} +
+ `; + } else { + // Non-image attachment + return ` +
+ +
+ `; + } +}; + +const scrollToNewMessages = (firstNewMessageIdentifier) => { + const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`); + if (newMessageElement) { + newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } +}; + +const updateLatestMessageIdentifiers = (room, mostRecentMessage) => { + latestMessageIdentifiers[room] = mostRecentMessage; + localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers)); +}; + +const handleReplyLogic = (fetchMessages) => { + const replyButtons = document.querySelectorAll(".reply-button"); + replyButtons.forEach(button => { + button.addEventListener("click", () => { + const replyToMessageIdentifier = button.dataset.messageIdentifier; + const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier); + if (repliedMessage) { + showReplyPreview(repliedMessage); + } + }); + }); +}; + +const showReplyPreview = (repliedMessage) => { + replyToMessageIdentifier = repliedMessage.identifier; + + const replyContainer = document.createElement("div"); + replyContainer.className = "reply-container"; + replyContainer.innerHTML = ` +
+ Replying to: ${repliedMessage.content} + +
+ `; + + if (!document.querySelector(".reply-container")) { + const messageInputSection = document.querySelector(".message-input-section"); + if (messageInputSection) { + messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild); + 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(); + } +}; + +const updatePaginationControls = async (room, limit) => { + const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room) + renderPaginationControls(room, totalMessages, limit); +}; + // Polling function to check for new messages without clearing existing ones diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js index 89e0ba7..a6b586c 100644 --- a/assets/js/QortalApi.js +++ b/assets/js/QortalApi.js @@ -645,31 +645,53 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => { }; -const searchAllCountOnly = async (query) => { +const searchAllCountOnly = async (query, room) => { try { let offset = 0; const limit = 100; // Chunk size for fetching let totalCount = 0; let hasMore = true; - + + if (room === "admins") { + while (hasMore) { + const response = await qortalRequest({ + action: "SEARCH_QDN_RESOURCES", + service: "MAIL_PRIVATE", + query: query, + limit: limit, + offset: offset, + mode: "ALL", + reverse: false + }); + + if (response && response.length > 0) { + totalCount += response.length; + offset = totalCount; + console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`); + } else { + hasMore = false; + } + } + }else { // Fetch in chunks to accumulate the count - while (hasMore) { - const response = await qortalRequest({ - action: "SEARCH_QDN_RESOURCES", - service: "BLOG_POST", - query: query, - limit: limit, - offset: offset, - mode: "ALL", - reverse: false - }); - - if (response && response.length > 0) { - totalCount += response.length; - offset += limit; - console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`); - } else { - hasMore = false; + while (hasMore) { + const response = await qortalRequest({ + action: "SEARCH_QDN_RESOURCES", + service: "BLOG_POST", + query: query, + limit: limit, + offset: offset, + mode: "ALL", + reverse: false + }); + + if (response && response.length > 0) { + totalCount += response.length; + offset = totalCount; + console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`); + } else { + hasMore = false; + } } } diff --git a/index.html b/index.html index f35a7c3..8c8e5fd 100644 --- a/index.html +++ b/index.html @@ -68,7 +68,7 @@ - Q-Mintership Alpha v0.54b
+ Q-Mintership Alpha v0.55b
@@ -263,7 +263,7 @@ - +