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 {
|
.comments-container {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
max-height: 150px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
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.
|
// 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 isEncryptedTestMode = true
|
||||||
const encryptedCardIdentifierPrefix = "test-MDC";
|
const encryptedCardIdentifierPrefix = "test-MDC"
|
||||||
let isExistingEncryptedCard = false;
|
let isExistingEncryptedCard = false
|
||||||
let existingDecryptedCardData = {};
|
let existingDecryptedCardData = {}
|
||||||
let existingEncryptedCardIdentifier = {};
|
let existingEncryptedCardIdentifier = {}
|
||||||
let cardMinterName = {}
|
let cardMinterName = {}
|
||||||
let existingCardMinterNames = []
|
let existingCardMinterNames = []
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ const loadAdminBoardPage = async () => {
|
|||||||
await publishEncryptedCard();
|
await publishEncryptedCard();
|
||||||
});
|
});
|
||||||
|
|
||||||
await createCardMinterNameList();
|
// await fetchAndValidateAllAdminCards();
|
||||||
await fetchAllEncryptedCards();
|
await fetchAllEncryptedCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,55 +102,75 @@ const extractCardsMinterName = (cardIdentifier) => {
|
|||||||
if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) {
|
if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) {
|
||||||
throw new Error('Invalid identifier format or prefix mismatch');
|
throw new Error('Invalid identifier format or prefix mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the identifier into parts
|
// Split the identifier into parts
|
||||||
const parts = cardIdentifier.split('-');
|
const parts = cardIdentifier.split('-');
|
||||||
|
|
||||||
// Ensure the format has at least 3 parts
|
// Ensure the format has at least 3 parts
|
||||||
if (parts.length < 3) {
|
if (parts.length < 3) {
|
||||||
throw new Error('Invalid identifier format');
|
throw new Error('Invalid identifier format');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract minterName (everything from the second part to the second-to-last part)
|
// Extract minterName (everything from the second part to the second-to-last part)
|
||||||
const minterName = parts.slice(2, -1).join('-');
|
const minterName = parts.slice(2, -1).join('-');
|
||||||
|
|
||||||
// Return the extracted minterName
|
// Return the extracted minterName
|
||||||
return minterName;
|
return minterName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processCards = async (validEncryptedCards) => {
|
||||||
|
const latestCardsMap = new Map()
|
||||||
|
|
||||||
const createCardMinterNameList = async () => {
|
// Step 1: Filter and keep the most recent card per identifier
|
||||||
|
validEncryptedCards.forEach(card => {
|
||||||
const response = await qortalRequest({
|
const timestamp = card.updated || card.created || 0
|
||||||
action: "SEARCH_QDN_RESOURCES",
|
const existingCard = latestCardsMap.get(card.identifier)
|
||||||
service: "MAIL_PRIVATE",
|
|
||||||
query: `${encryptedCardIdentifierPrefix}`,
|
|
||||||
mode: "ALL",
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatedEncryptedCards = await Promise.all(
|
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
|
||||||
response.map(async card => {
|
latestCardsMap.set(card.identifier, card)
|
||||||
const isValid = await validateEncryptedCardIdentifier(card);
|
}
|
||||||
return isValid ? card : null;
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
|
// Step 2: Extract unique cards
|
||||||
|
const uniqueValidCards = Array.from(latestCardsMap.values())
|
||||||
|
|
||||||
if (validEncryptedCards.length === 0) {
|
// Step 3: Group by minterName and select the most recent card per minterName
|
||||||
console.log(`no matches found, not adding any names to name list.`)
|
const minterNameMap = new Map()
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const result of validEncryptedCards) {
|
for (const card of validEncryptedCards) {
|
||||||
const minterName = await extractCardsMinterName(result.identifier)
|
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)) {
|
if (!existingCardMinterNames.includes(minterName)) {
|
||||||
existingCardMinterNames.push(minterName)
|
existingCardMinterNames.push(minterName)
|
||||||
console.log(`cardsMinterName: ${minterName} - added to list`)
|
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 ----------------------------------------
|
//Main function to load the Minter Cards ----------------------------------------
|
||||||
const fetchAllEncryptedCards = async () => {
|
const fetchAllEncryptedCards = async () => {
|
||||||
@ -179,40 +199,22 @@ const fetchAllEncryptedCards = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
|
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
|
||||||
|
|
||||||
if (validEncryptedCards.length === 0) {
|
if (validEncryptedCards.length === 0) {
|
||||||
encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>";
|
encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const finalCards = await processCards(validEncryptedCards)
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Display skeleton cards immediately
|
// Display skeleton cards immediately
|
||||||
encryptedCardsContainer.innerHTML = "";
|
encryptedCardsContainer.innerHTML = "";
|
||||||
uniqueValidCards.forEach(card => {
|
finalCards.forEach(card => {
|
||||||
const skeletonHTML = createSkeletonCardHTML(card.identifier);
|
const skeletonHTML = createSkeletonCardHTML(card.identifier);
|
||||||
encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
|
encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch and update each card
|
// Fetch and update each card
|
||||||
uniqueValidCards.forEach(async card => {
|
finalCards.forEach(async card => {
|
||||||
try {
|
try {
|
||||||
const cardDataResponse = await qortalRequest({
|
const cardDataResponse = await qortalRequest({
|
||||||
action: "FETCH_QDN_RESOURCE",
|
action: "FETCH_QDN_RESOURCE",
|
||||||
@ -240,9 +242,9 @@ const fetchAllEncryptedCards = async () => {
|
|||||||
// Fetch poll results
|
// Fetch poll results
|
||||||
const pollResults = await fetchPollResults(decryptedCardData.poll);
|
const pollResults = await fetchPollResults(decryptedCardData.poll);
|
||||||
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
|
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
|
||||||
|
const commentCount = await getCommentCount(card.identifier);
|
||||||
// Generate final card HTML
|
// 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);
|
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing card ${card.identifier}:`, 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. ---------------------------------
|
// Post a comment on a card. ---------------------------------
|
||||||
const postEncryptedComment = async (cardIdentifier) => {
|
const postEncryptedComment = async (cardIdentifier) => {
|
||||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||||
@ -498,7 +516,7 @@ const postEncryptedComment = async (cardIdentifier) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const postTimestamp = `${Date.now()}`
|
const postTimestamp = Date.now()
|
||||||
console.log(`timestmp to be posted: ${postTimestamp}`)
|
console.log(`timestmp to be posted: ${postTimestamp}`)
|
||||||
|
|
||||||
const commentData = {
|
const commentData = {
|
||||||
@ -575,7 +593,8 @@ const displayEncryptedComments = async (cardIdentifier) => {
|
|||||||
|
|
||||||
const decryptedCommentData = await decryptAndParseObject(commentDataResponse)
|
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.
|
//TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section.
|
||||||
const commentHTML = `
|
const commentHTML = `
|
||||||
@ -688,7 +707,7 @@ const processQortalLinkForRendering = async (link) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the overall Minter Card HTML -----------------------------------------------
|
// 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 { minterName, header, content, links, creator, timestamp, poll } = cardData;
|
||||||
const formattedDate = new Date(timestamp).toLocaleString();
|
const formattedDate = new Date(timestamp).toLocaleString();
|
||||||
const minterAvatar = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
|
const minterAvatar = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
|
||||||
@ -740,7 +759,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier) =>
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="actions-buttons">
|
<div class="actions-buttons">
|
||||||
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
|
<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>
|
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -554,281 +554,330 @@ const generateAttachmentID = (room, fileIndex = null) => {
|
|||||||
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
|
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptFile = async (encryptedData) => {
|
// const decryptFile = async (encryptedData) => {
|
||||||
const publicKey = await getPublicKeyByName(userState.accountName)
|
// const publicKey = await getPublicKeyByName(userState.accountName)
|
||||||
const response = await qortalRequest({
|
// const response = await qortalRequest({
|
||||||
action: 'DECRYPT_DATA',
|
// action: 'DECRYPT_DATA',
|
||||||
encryptedData, // has to be in base64 format
|
// 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.
|
// // publicKey: publicKey // requires the public key of the opposite user with whom you've created the encrypted data.
|
||||||
});
|
// });
|
||||||
const decryptedObject = response
|
// const decryptedObject = response
|
||||||
return decryptedObject
|
// return decryptedObject
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// --- REFACTORED LOAD MESSAGES AND HELPER FUNCTIONS ---
|
||||||
|
|
||||||
const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||||
try {
|
try {
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = page * limit;
|
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");
|
const messagesContainer = document.querySelector("#messages-container");
|
||||||
if (!messagesContainer) return;
|
if (!messagesContainer) return;
|
||||||
|
|
||||||
// If not polling, clear the message container and the existing identifiers for a fresh load
|
prepareMessageContainer(messagesContainer, isPolling);
|
||||||
if (!isPolling) {
|
|
||||||
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
|
|
||||||
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the set of existing identifiers from the messages container
|
const { service, query } = getServiceAndQuery(room);
|
||||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
const response = await fetchResourceList(service, query, limit, offset, room);
|
||||||
|
|
||||||
// Fetch messages for the current room and page
|
console.log(`Fetched ${response.length} message(s) for page ${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}`);
|
|
||||||
|
|
||||||
if (response.length === 0) {
|
if (handleNoMessagesScenario(isPolling, page, response, messagesContainer)) {
|
||||||
// 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>`;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
// Re-establish existing identifiers after preparing container
|
||||||
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
existingIdentifiers = new Set(
|
||||||
let firstNewMessageIdentifier = null
|
Array.from(messagesContainer.querySelectorAll('.message-item'))
|
||||||
|
.map(item => item.dataset.identifier)
|
||||||
// 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: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render new messages without duplication
|
let mostRecentMessage = getCurrentMostRecentMessage(room);
|
||||||
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 attachmentHtml = "";
|
const fetchMessages = await fetchAllMessages(response, service, room);
|
||||||
if (message.attachments && message.attachments.length > 0) {
|
const { firstNewMessageIdentifier, updatedMostRecentMessage } = renderNewMessages(
|
||||||
for (const attachment of message.attachments) {
|
fetchMessages,
|
||||||
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
existingIdentifiers,
|
||||||
try {
|
messagesContainer,
|
||||||
// Construct the image URL
|
room,
|
||||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
|
mostRecentMessage
|
||||||
|
);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstNewMessageIdentifier && !isPolling) {
|
if (firstNewMessageIdentifier && !isPolling) {
|
||||||
// Scroll to the first new message
|
scrollToNewMessages(firstNewMessageIdentifier);
|
||||||
const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`);
|
|
||||||
if (newMessageElement) {
|
|
||||||
newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update latestMessageIdentifiers for the room
|
if (updatedMostRecentMessage) {
|
||||||
if (mostRecentMessage) {
|
updateLatestMessageIdentifiers(room, updatedMostRecentMessage);
|
||||||
latestMessageIdentifiers[room] = mostRecentMessage;
|
|
||||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners to the reply buttons
|
handleReplyLogic(fetchMessages);
|
||||||
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) {
|
await updatePaginationControls(room, limit);
|
||||||
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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading messages from QDN:', 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
|
// 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 {
|
try {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const limit = 100; // Chunk size for fetching
|
const limit = 100; // Chunk size for fetching
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
let hasMore = true;
|
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
|
// Fetch in chunks to accumulate the count
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
const response = await qortalRequest({
|
const response = await qortalRequest({
|
||||||
action: "SEARCH_QDN_RESOURCES",
|
action: "SEARCH_QDN_RESOURCES",
|
||||||
service: "BLOG_POST",
|
service: "BLOG_POST",
|
||||||
query: query,
|
query: query,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
mode: "ALL",
|
mode: "ALL",
|
||||||
reverse: false
|
reverse: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && response.length > 0) {
|
if (response && response.length > 0) {
|
||||||
totalCount += response.length;
|
totalCount += response.length;
|
||||||
offset += limit;
|
offset = totalCount;
|
||||||
console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`);
|
console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`);
|
||||||
} else {
|
} else {
|
||||||
hasMore = false;
|
hasMore = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</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>
|
</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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<a class="link-wrap" href="#">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
|
Loading…
Reference in New Issue
Block a user