MASSIVE re-factor on both the forum AND Admin Board... MANY changes made, too many to list. Multiple bugfixes, completely modified code structure, etc. Details will be added at a later date. Until then, see code. ;)
This commit is contained in:
parent
25c0afb237
commit
759f000a00
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1011,7 +1011,7 @@ body {
|
||||
|
||||
.comments-container {
|
||||
margin-bottom: 10px;
|
||||
max-height: 150px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
@ -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 = "<p>No valid cards found.</p>";
|
||||
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) =>
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
|
||||
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS</button>
|
||||
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
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: "<em>Encrypted message cannot be displayed</em>",
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: null,
|
||||
timestamp,
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
messageObject = messageResponse;
|
||||
}
|
||||
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject?.messageHtml || "<em>Message content missing</em>",
|
||||
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: "<em>Error loading message</em>",
|
||||
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 = `
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
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 += `<div class="attachment">
|
||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
|
||||
</div>`;
|
||||
|
||||
// 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 += `<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
<div class="message-header" style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="${avatarUrl}" alt="Avatar" class="user-avatar" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;">
|
||||
<span class="username">${message.name}</span>
|
||||
${isNewMessage ? `<span class="new-indicator" style="margin-left: 10px; color: red; font-weight: bold;">NEW</span>` : ''}
|
||||
</div>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
${replyHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<div class="attachments-gallery">
|
||||
${attachmentHtml}
|
||||
</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);
|
||||
|
||||
// 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 = `
|
||||
<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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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 = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
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 || "<em>Message content missing</em>",
|
||||
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: "<em>Error loading message</em>",
|
||||
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 `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
<div class="message-header" style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="${avatarUrl}" alt="Avatar" class="user-avatar" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;">
|
||||
<span class="username">${message.name}</span>
|
||||
${isNewMessage ? `<span class="new-indicator" style="margin-left: 10px; color: red; font-weight: bold;">NEW</span>` : ''}
|
||||
</div>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
${replyHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<div class="attachments-gallery">
|
||||
${attachmentHtml}
|
||||
</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const buildReplyHtml = (message, fetchMessages) => {
|
||||
if (!message.replyTo) return "";
|
||||
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (!repliedMessage) return "";
|
||||
|
||||
return `
|
||||
<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 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 `
|
||||
<div class="attachment">
|
||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Non-image attachment
|
||||
return `
|
||||
<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
|
||||
Download ${attachment.filename}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
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 = `
|
||||
<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);
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.54b<br></a></span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.55b<br></a></span>
|
||||
</div>
|
||||
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
|
||||
|
||||
@ -263,7 +263,7 @@
|
||||
</div>
|
||||
|
||||
<a class="link-wrap" href="#">
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.54beta</p>
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.55beta</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
Loading…
Reference in New Issue
Block a user