Fixed no names, and fake names getting displayed on MinterBoard, fixed image display in Admin Room, and fixed QuickMythril 'poll hijack' issue. Minor code cleanup. v0.66beta.

This commit is contained in:
crowetic 2024-12-30 21:39:18 -08:00
parent cfccfab99a
commit 5a6baaef66
5 changed files with 403 additions and 390 deletions

View File

@ -11,7 +11,7 @@ let attemptLoadAdminDataCount = 0
let adminMemberCount = 0
let adminPublicKeys = []
console.log("Attempting to load AdminBoard.js");
console.log("Attempting to load AdminBoard.js")
const loadAdminBoardPage = async () => {
// Clear existing content on the page
@ -24,7 +24,7 @@ const loadAdminBoardPage = async () => {
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
const mainContent = document.createElement("div")
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">AdminBoard</h1>
@ -56,8 +56,8 @@ const loadAdminBoardPage = async () => {
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
`
document.body.appendChild(mainContent)
const publishCardButton = document.getElementById("publish-card-button")
if (publishCardButton) {
publishCardButton.addEventListener("click", async () => {
@ -97,16 +97,16 @@ const loadAdminBoardPage = async () => {
}
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
const isTopicChecked = document.getElementById("topic-checkbox").checked;
event.preventDefault()
const isTopicChecked = document.getElementById("topic-checkbox").checked
// Pass that boolean to publishEncryptedCard
await publishEncryptedCard(isTopicChecked);
});
await publishEncryptedCard(isTopicChecked)
})
// await fetchAndValidateAllAdminCards();
await fetchAllEncryptedCards();
await updateOrSaveAdminGroupsDataLocally();
// await fetchAndValidateAllAdminCards()
await fetchAllEncryptedCards()
await updateOrSaveAdminGroupsDataLocally()
}
// Example: fetch and save admin public keys and count
@ -131,7 +131,7 @@ const updateOrSaveAdminGroupsDataLocally = async () => {
console.error('Error fetching/storing admin public keys:', error)
attemptLoadAdminDataCount++
}
};
}
const loadOrFetchAdminGroupsData = async () => {
try {
@ -163,11 +163,6 @@ const loadOrFetchAdminGroupsData = async () => {
}
const extractEncryptedCardsMinterName = (cardIdentifier) => {
// Ensure the identifier starts with the prefix
// 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) {
@ -179,9 +174,9 @@ const extractEncryptedCardsMinterName = (cardIdentifier) => {
return
}
// 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 minterName;
return minterName
}
const processCards = async (validEncryptedCards) => {
@ -206,24 +201,17 @@ const processCards = async (validEncryptedCards) => {
}
//Main function to load the Minter Cards ----------------------------------------
const fetchAllEncryptedCards = async () => {
const encryptedCardsContainer = document.getElementById("encrypted-cards-container");
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>";
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
try {
// const response = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// service: "MAIL_PRIVATE",
// query: encryptedCardIdentifierPrefix,
// mode: "ALL"
// })
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0)
if (!response || !Array.isArray(response) || response.length === 0) {
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>";
return;
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
return
}
// Validate cards and filter
@ -235,7 +223,7 @@ const fetchAllEncryptedCards = async () => {
)
console.log(`validatedEncryptedCards:`, validatedEncryptedCards, `... running next filter...`)
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null)
console.log(`validEncryptedcards:`, validEncryptedCards)
if (validEncryptedCards.length === 0) {
@ -246,80 +234,89 @@ const fetchAllEncryptedCards = async () => {
console.log(`finalCards:`,finalCards)
// Display skeleton cards immediately
encryptedCardsContainer.innerHTML = "";
encryptedCardsContainer.innerHTML = ""
finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier);
encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
});
const skeletonHTML = createSkeletonCardHTML(card.identifier)
encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
})
// Fetch and update each card
finalCards.forEach(async card => {
try {
const hasMinterName = await extractEncryptedCardsMinterName(card.identifier)
if (hasMinterName) existingCardMinterNames.push(hasMinterName)
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "MAIL_PRIVATE",
identifier: card.identifier,
encoding: "base64"
});
})
if (!cardDataResponse) {
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`);
removeSkeleton(card.identifier);
return;
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
removeSkeleton(card.identifier)
return
}
const decryptedCardData = await decryptAndParseObject(cardDataResponse);
const decryptedCardData = await decryptAndParseObject(cardDataResponse)
// Skip cards without polls
if (!decryptedCardData.poll) {
console.warn(`Skipping card with no poll: ${card.identifier}`);
removeSkeleton(card.identifier);
return;
console.warn(`Skipping card with no poll: ${card.identifier}`)
removeSkeleton(card.identifier)
return
}
const encryptedCardPollPublisherPublicKey = await getPollPublisherPublicKey(decryptedCardData.poll)
const encryptedCardPublisherPublicKey = await getPublicKeyByName(card.name)
if (encryptedCardPollPublisherPublicKey != encryptedCardPublisherPublicKey) {
console.warn(`QuickMythril cardPollHijack attack found! Not including card with identifier: ${card.identifier}`)
return
}
// Fetch poll results
const pollResults = await fetchPollResults(decryptedCardData.poll);
const pollResults = await fetchPollResults(decryptedCardData.poll)
if (pollResults?.error) {
console.warn(`Skipping card with non-existent poll: ${card.identifier}, poll=${decryptedCardData.poll}`);
removeEncryptedSkeleton(card.identifier);
return;
console.warn(`Skipping card with non-existent poll: ${card.identifier}, poll=${decryptedCardData.poll}`)
removeSkeleton(card.identifier)
return
}
// const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier)
// Generate final card HTML
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount);
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount)
replaceEncryptedSkeleton(card.identifier, finalCardHTML)
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error);
removeEncryptedSkeleton(card.identifier); // Silently remove skeleton on error
console.error(`Error processing card ${card.identifier}:`, error)
removeSkeleton(card.identifier)
}
});
})
} catch (error) {
console.error("Error loading cards:", error);
encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>";
console.error("Error loading cards:", error)
encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>"
}
};
}
const removeEncryptedSkeleton = (cardIdentifier) => {
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (encryptedSkeletonCard) {
encryptedSkeletonCard.remove(); // Remove the skeleton silently
}
};
}
const replaceEncryptedSkeleton = (cardIdentifier, htmlContent) => {
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (encryptedSkeletonCard) {
encryptedSkeletonCard.outerHTML = htmlContent;
}
};
}
// Function to create a skeleton card
const createEncryptedSkeletonCardHTML = (cardIdentifier) => {
@ -336,46 +333,37 @@ const createEncryptedSkeletonCardHTML = (cardIdentifier) => {
<div style="width: 100%; height: 40px; background-color: #eee;"></div>
</div>
</div>
`;
};
`
}
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
const fetchExistingEncryptedCard = async (minterName) => {
try {
// const response = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// service: "MAIL_PRIVATE",
// identifier: encryptedCardIdentifierPrefix,
// query: minterName,
// mode: "ALL",
// })
//CHANGED to searchSimple to speed up search results.
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, minterName, 0)
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`)
// Step 2: Check if the response is an array and not empty
if (!response || !Array.isArray(response) || response.length === 0) {
console.log("No cards found for the current user.");
return null;
console.log("No cards found for the current user.")
return null
}
// Step 3: Validate cards asynchronously
const validatedCards = await Promise.all(
response.map(async card => {
const isValid = await validateEncryptedCardIdentifier(card)
return isValid ? card : null;
return isValid ? card : null
})
);
)
// Step 4: Filter out invalid cards
const validCards = validatedCards.filter(card => card !== null);
const validCards = validatedCards.filter(card => card !== null)
if (validCards.length > 0) {
// Step 5: Sort by most recent timestamp
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0];
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]
// Step 6: Fetch full card data
const cardDataResponse = await qortalRequest({
@ -384,23 +372,23 @@ const fetchExistingEncryptedCard = async (minterName) => {
service: mostRecentCard.service,
identifier: mostRecentCard.identifier,
encoding: "base64"
});
})
existingEncryptedCardIdentifier = mostRecentCard.identifier;
existingEncryptedCardIdentifier = mostRecentCard.identifier
existingDecryptedCardData = await decryptAndParseObject(cardDataResponse)
console.log("Full card data fetched successfully:", existingDecryptedCardData);
console.log("Full card data fetched successfully:", existingDecryptedCardData)
return existingDecryptedCardData;
return existingDecryptedCardData
}
console.log("No valid cards found.");
return null;
console.log("No valid cards found.")
return null
} catch (error) {
console.error("Error fetching existing card:", error);
return null;
return null
}
};
}
// Validate that a card is indeed a card and not a comment. -------------------------------------
const validateEncryptedCardIdentifier = async (card) => {
@ -410,7 +398,7 @@ const validateEncryptedCardIdentifier = async (card) => {
card.service === "MAIL_PRIVATE" &&
card.identifier && !card.identifier.includes("comment") &&
card.created
);
)
}
// Load existing card data passed, into the form for editing -------------------------------------
@ -421,15 +409,15 @@ const loadEncryptedCardIntoForm = async () => {
document.getElementById("card-header").value = existingDecryptedCardData.header
document.getElementById("card-content").value = existingDecryptedCardData.content
const linksContainer = document.getElementById("links-container");
const linksContainer = document.getElementById("links-container")
linksContainer.innerHTML = ""; // Clear previous links
existingDecryptedCardData.links.forEach(link => {
const linkInput = document.createElement("input");
linkInput.type = "text";
linkInput.className = "card-link";
linkInput.value = link;
linksContainer.appendChild(linkInput);
});
const linkInput = document.createElement("input")
linkInput.type = "text"
linkInput.className = "card-link"
linkInput.value = link
linksContainer.appendChild(linkInput)
})
}
}
@ -447,20 +435,20 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
// If the user wants it to be a topic, we set global isTopic = true, else false
isTopic = isTopicModePassed || isTopic
const minterNameInput = document.getElementById("minter-name-input").value.trim();
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const minterNameInput = document.getElementById("minter-name-input").value.trim()
const header = document.getElementById("card-header").value.trim()
const content = document.getElementById("card-content").value.trim()
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
.filter(link => link.startsWith("qortal://"))
// Basic validation
if (!header || !content) {
alert("Header and Content are required!");
return;
alert("Header and Content are required!")
return
}
let publishedMinterName = minterNameInput;
let publishedMinterName = minterNameInput
// If not topic mode, validate the user actually entered a valid Minter name
if (!isTopic) {
@ -507,9 +495,9 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
try {
// Convert to base64 or fallback
let base64CardData = await objectToBase64(cardData);
let base64CardData = await objectToBase64(cardData)
if (!base64CardData) {
base64CardData = btoa(JSON.stringify(cardData));
base64CardData = btoa(JSON.stringify(cardData))
}
let verifiedAdminPublicKeys = adminPublicKeys
@ -560,15 +548,15 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
alert("Card updated successfully! (No poll updates possible currently...)");
}
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("encrypted-cards-container").style.display = "flex";
document.getElementById("publish-card-form").reset()
document.getElementById("publish-card-view").style.display = "none"
document.getElementById("encrypted-cards-container").style.display = "flex"
isTopic = false; // reset global
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
console.error("Error publishing card or poll:", error)
alert("Failed to publish card and poll.")
}
};
}
const getEncryptedCommentCount = async (cardIdentifier) => {
@ -633,7 +621,7 @@ const postEncryptedComment = async (cardIdentifier) => {
//Fetch the comments for a card with passed card identifier ----------------------------
const fetchEncryptedComments = async (cardIdentifier) => {
try {
const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0)
const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0, 0, '', false)
if (response) {
return response;
}

View File

@ -140,7 +140,6 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
const processMinterCards = async (validMinterCards) => {
const latestCardsMap = new Map()
// Step 1: Filter and keep the most recent card per identifier
validMinterCards.forEach(card => {
const timestamp = card.updated || card.created || 0
const existingCard = latestCardsMap.get(card.identifier)
@ -150,36 +149,47 @@ const processMinterCards = async (validMinterCards) => {
}
})
// Step 2: Extract unique cards
const uniqueValidCards = Array.from(latestCardsMap.values())
// Step 3: Group by minterName and select the most recent card per minterName
const minterGroupMembers = await fetchMinterGroupMembers()
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
const minterNameMap = new Map()
for (const card of validMinterCards) {
const minterName = await extractMinterCardsMinterName(card.identifier)
console.log(`minterName`, minterName)
const minterNameInfo = await getNameInfo(minterName)
if (!minterNameInfo) {
console.warn(`minterNameInfo is null for minter: ${minterName}`)
continue
}
const minterAddress = await minterNameInfo.owner
if (!minterAddress) {
console.warn(`minterAddress is FAKE or INVALID in some way! minter: ${minterName}`)
continue
} else if (minterGroupAddresses.includes(minterAddress)){
console.log(`existing minter FOUND and/or FAKE NAME FOUND (if following is null then fake name: ${minterAddress}), not including minter card: ${card.identifier}`)
continue
}
const existingCard = minterNameMap.get(minterName)
const cardTimestamp = card.updated || card.created || 0
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
// 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
seenMinterNames.add(minterName)
}
}
// 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
@ -191,54 +201,41 @@ const processMinterCards = async (validMinterCards) => {
//Main function to load the Minter Cards ----------------------------------------
const loadCards = async () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Loading cards...</p>"
try {
// const response = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// service: "BLOG_POST",
// query: cardIdentifierPrefix,
// mode: "ALL"
// })
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0)
if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
cardsContainer.innerHTML = "<p>No cards found.</p>"
return;
}
// Validate cards and filter
const validatedCards = await Promise.all(
response.map(async card => {
const isValid = await validateCardStructure(card);
return isValid ? card : null;
const isValid = await validateCardStructure(card)
return isValid ? card : null
})
);
const validCards = validatedCards.filter(card => card !== null);
const validCards = validatedCards.filter(card => card !== null)
if (validCards.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>";
return;
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
return
}
const finalCards = await processMinterCards(validCards)
// Sort cards by timestamp descending (newest first)
// validCards.sort((a, b) => {
// const timestampA = a.updated || a.created || 0;
// const timestampB = b.updated || b.created || 0;
// return timestampB - timestampA;
// });
// Display skeleton cards immediately
cardsContainer.innerHTML = "";
cardsContainer.innerHTML = ""
finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
});
const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
})
// Fetch and update each card
finalCards.forEach(async card => {
@ -248,57 +245,62 @@ const loadCards = async () => {
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
})
if (!cardDataResponse) {
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`);
removeSkeleton(card.identifier);
return;
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
removeSkeleton(card.identifier)
return
}
// Skip cards without polls
if (!cardDataResponse.poll) {
console.warn(`Skipping card with no poll: ${card.identifier}`);
removeSkeleton(card.identifier);
return;
console.warn(`Skipping card with no poll: ${card.identifier}`)
removeSkeleton(card.identifier)
return
}
// Fetch poll results
const pollResults = await fetchPollResults(cardDataResponse.poll);
const pollPublisherPublicKey = await getPollPublisherPublicKey(cardDataResponse.poll)
const cardPublisherPublicKey = await getPublicKeyByName(card.name)
if (pollPublisherPublicKey != cardPublisherPublicKey) {
console.warn(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`)
removeSkeleton(card.identifier)
return
}
const pollResults = await fetchPollResults(cardDataResponse.poll)
const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML
const commentCount = await countComments(card.identifier)
const cardUpdatedTime = card.updated || null
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, BgColor);
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, BgColor)
replaceSkeleton(card.identifier, finalCardHTML);
replaceSkeleton(card.identifier, finalCardHTML)
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error);
removeSkeleton(card.identifier); // Silently remove skeleton on error
console.error(`Error processing card ${card.identifier}:`, error)
removeSkeleton(card.identifier)
}
});
})
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
console.error("Error loading cards:", error)
cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
}
};
}
const removeSkeleton = (cardIdentifier) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) {
skeletonCard.remove(); // Remove the skeleton silently
skeletonCard.remove()
}
};
}
const replaceSkeleton = (cardIdentifier, htmlContent) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) {
skeletonCard.outerHTML = htmlContent;
skeletonCard.outerHTML = htmlContent
}
};
}
// Function to create a skeleton card
const createSkeletonCardHTML = (cardIdentifier) => {
return `
<div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;">
@ -314,28 +316,16 @@ const createSkeletonCardHTML = (cardIdentifier) => {
<div style="width: 100%; height: 80px; background-color: #eee; color:rgb(17, 24, 28); padding: 0.22vh"><p>PLEASE BE PATIENT</p><p style="color: #11121c"> While data loads from QDN...</div>
</div>
</div>
`;
};
`
}
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
const fetchExistingCard = async () => {
try {
// Step 1: Perform the search
// const response = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// service: "BLOG_POST",
// identifier: cardIdentifierPrefix,
// name: userState.accountName,
// mode: "ALL",
// exactMatchNames: true // Search for the exact userName only when finding existing cards
// })
// Changed to searchSimple to improve load times.
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, `${userState.accountName}`, 0)
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`)
// Step 2: Check if the response is an array and not empty
if (!response || !Array.isArray(response) || response.length === 0) {
console.log("No cards found for the current user.")
return null
@ -343,7 +333,6 @@ const fetchExistingCard = async () => {
return response[0]
}
// Validate cards asynchronously, check that they are not comments, etc.
const validatedCards = await Promise.all(
response.map(async card => {
const isValid = await validateCardStructure(card)
@ -351,14 +340,12 @@ const fetchExistingCard = async () => {
})
)
// Filter out invalid cards
const validCards = validatedCards.filter(card => card !== null)
if (validCards.length > 0) {
// Sort by most recent timestamp
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]
// Fetch full card data
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: userState.accountName, // User's account name
@ -413,17 +400,16 @@ const loadCardIntoForm = async (cardData) => {
// Main function to publish a new Minter Card -----------------------------------------------
const publishCard = async () => {
const minterGroupData = await fetchMinterGroupMembers();
const minterGroupAddresses = minterGroupData.map(m => m.member); // array of addresses
const minterGroupData = await fetchMinterGroupMembers()
const minterGroupAddresses = minterGroupData.map(m => m.member)
// 2) check if user is a minter
const userAddress = userState.accountAddress;
if (minterGroupAddresses.includes(userAddress)) {
alert("You are already a Minter and cannot publish a new card!");
alert("You are already a Minter and cannot publish a new card!")
return;
}
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const header = document.getElementById("card-header").value.trim()
const content = document.getElementById("card-content").value.trim()
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"))
@ -776,7 +762,7 @@ const toggleComments = async (cardIdentifier) => {
if (isHidden) {
// Show comments
commentButton.textContent = "LOADING..."
await displayComments(cardIdentifier);
await displayComments(cardIdentifier)
commentsSection.style.display = 'block'
// Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS'

View File

@ -448,48 +448,48 @@ const setupFileInputs = (room) => {
addToPublishButton.disabled = selectedImages.length === 0;
};
const container = document.createElement('div');
container.style = "display: flex; flex-direction: column; align-items: center; margin: 5px;";
container.append(img, removeButton);
previewContainer.append(container);
};
reader.readAsDataURL(file);
});
});
const container = document.createElement('div')
container.style = "display: flex; flex-direction: column; align-items: center; margin: 5px;"
container.append(img, removeButton)
previewContainer.append(container)
}
reader.readAsDataURL(file)
})
})
addToPublishButton.addEventListener('click', () => {
processSelectedImages(selectedImages, multiResource, room);
selectedImages = [];
imageFileInput.value = "";
addToPublishButton.disabled = true;
});
processSelectedImages(selectedImages, multiResource, room)
selectedImages = []
imageFileInput.value = ""
addToPublishButton.disabled = true
})
fileInput.addEventListener('change', (event) => {
selectedFiles = [...event.target.files];
});
selectedFiles = [...event.target.files]
})
sendButton.addEventListener('click', async () => {
const quill = new Quill('#editor');
const messageHtml = quill.root.innerHTML.trim();
const quill = new Quill('#editor')
const messageHtml = quill.root.innerHTML.trim()
if (messageHtml || selectedFiles.length > 0 || selectedImages.length > 0) {
await handleSendMessage(room, messageHtml, selectedFiles, selectedImages, multiResource);
await handleSendMessage(room, messageHtml, selectedFiles, selectedImages, multiResource)
}
});
};
})
}
// Process selected images
const processSelectedImages = async (selectedImages, multiResource, room) => {
for (const file of selectedImages) {
const attachmentID = generateAttachmentID(room, selectedImages.indexOf(file));
const attachmentID = generateAttachmentID(room, selectedImages.indexOf(file))
multiResource.push({
name: userState.accountName,
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
identifier: attachmentID,
file: file, // Use encrypted file for admins
});
})
attachmentIdentifiers.push({
name: userState.accountName,
@ -497,37 +497,33 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
identifier: attachmentID,
filename: file.name,
mimeType: file.type,
});
})
}
};
}
// Handle send message
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
const messageIdentifier = room === "admins"
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
: `${messageIdentifierPrefix}-${room}-${randomID()}`;
// const checkedAdminPublicKeys = room === "admins" && userState.isAdmin
// ? adminPublicKeys
// : await loadOrFetchAdminGroupsData().publicKeys;
: `${messageIdentifierPrefix}-${room}-${randomID()}`
try {
// Process selected images
if (selectedImages.length > 0) {
await processSelectedImages(selectedImages, multiResource, room);
await processSelectedImages(selectedImages, multiResource, room)
}
// Process selected files
if (selectedFiles && selectedFiles.length > 0) {
for (const file of selectedFiles) {
const attachmentID = generateAttachmentID(room, selectedFiles.indexOf(file));
const attachmentID = generateAttachmentID(room, selectedFiles.indexOf(file))
multiResource.push({
name: userState.accountName,
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
identifier: attachmentID,
file: file, // Use encrypted file for admins
});
})
attachmentIdentifiers.push({
name: userState.accountName,
@ -535,7 +531,7 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
identifier: attachmentID,
filename: file.name,
mimeType: file.type,
});
})
}
}
@ -545,16 +541,16 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
hasAttachment: multiResource.length > 0,
attachments: attachmentIdentifiers,
replyTo: replyToMessageIdentifier || null, // Include replyTo if applicable
};
}
// Encode the message object
let base64Message = await objectToBase64(messageObject);
let base64Message = await objectToBase64(messageObject)
if (!base64Message) {
base64Message = btoa(JSON.stringify(messageObject));
base64Message = btoa(JSON.stringify(messageObject))
}
if (room === "admins" && userState.isAdmin) {
console.log("Encrypting message for admins...");
console.log("Encrypting message for admins...")
multiResource.push({
name: userState.accountName,
@ -574,76 +570,71 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
// Publish resources
if (room === "admins") {
if (!userState.isAdmin) {
console.error("User is not an admin or no admin public keys found. Aborting publish.");
window.alert("You are not authorized to post in the Admin room.");
return;
console.error("User is not an admin or no admin public keys found. Aborting publish.")
window.alert("You are not authorized to post in the Admin room.")
return
}
console.log("Publishing encrypted resources for Admin room...");
await publishMultipleResources(multiResource, adminPublicKeys, true);
console.log("Publishing encrypted resources for Admin room...")
await publishMultipleResources(multiResource, adminPublicKeys, true)
} else {
console.log("Publishing resources for non-admin room...");
await publishMultipleResources(multiResource);
console.log("Publishing resources for non-admin room...")
await publishMultipleResources(multiResource)
}
// Clear inputs and show success notification
clearInputs();
showSuccessNotification();
clearInputs()
showSuccessNotification()
} catch (error) {
console.error("Error sending message:", error);
console.error("Error sending message:", error)
}
};
}
// Modify clearInputs to reset replyTo
const clearInputs = () => {
function clearInputs() {
const quill = new Quill('#editor');
quill.root.innerHTML = "";
// Properly reset Quill editor to ensure formatting options don't linger across messages
quill.setContents([]);
quill.setSelection(0,0);
// clear the local file input arrays
document.getElementById('file-input').value = "";
document.getElementById('image-input').value = "";
document.getElementById('preview-container').innerHTML = "";
replyToMessageIdentifier = null;
multiResource = [];
attachmentIdentifiers = [];
selectedImages = []
selectedFiles = []
selectedImages = [];
selectedFiles = [];
// Remove the reply containers
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove();
}
};
}
// Show success notification
const showSuccessNotification = () => {
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Please wait for confirmation.";
notification.style.color = "green";
notification.style.marginTop = "1em";
const notification = document.createElement('div')
notification.innerText = "Message published successfully! Please wait for confirmation."
notification.style.color = "green"
notification.style.marginTop = "1em"
document.querySelector(".message-input-section").appendChild(notification);
alert(`Successfully Published! Please note that messages will not display until after they are CONFIRMED, be patient!`)
setTimeout(() => {
notification.remove();
}, 10000);
};
notification.remove()
}, 10000)
}
// Generate unique attachment ID
const generateAttachmentID = (room, fileIndex = null) => {
const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${randomID()}` : `${messageAttachmentIdentifierPrefix}-${room}-${randomID()}`;
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 baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${randomID()}` : `${messageAttachmentIdentifierPrefix}-${room}-${randomID()}`
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID
}
// --- REFACTORED LOAD MESSAGES AND HELPER FUNCTIONS ---
@ -652,10 +643,10 @@ const findMessagePage = async (room, identifier, limit) => {
//TODO check that searchSimple change worked.
const allMessages = await searchSimple(service, query, '', 0, 0, room, 'false')
const idx = allMessages.findIndex(msg => msg.identifier === identifier);
const idx = allMessages.findIndex(msg => msg.identifier === identifier)
if (idx === -1) {
// Not found, default to last page or page=0
return 0;
return 0
}
return Math.floor(idx / limit)
@ -664,37 +655,37 @@ const findMessagePage = async (room, identifier, limit) => {
const loadMessagesFromQDN = async (room, page, isPolling = false) => {
try {
const limit = 10;
const offset = page * limit;
console.log(`Loading messages from QDN: room=${room}, page=${page}, offset=${offset}, limit=${limit}`);
const limit = 10
const offset = page * limit
console.log(`Loading messages from QDN: room=${room}, page=${page}, offset=${offset}, limit=${limit}`)
const messagesContainer = document.querySelector("#messages-container");
if (!messagesContainer) return;
const messagesContainer = document.querySelector("#messages-container")
if (!messagesContainer) return
prepareMessageContainer(messagesContainer, isPolling);
prepareMessageContainer(messagesContainer, isPolling)
const { service, query } = getServiceAndQuery(room);
const response = await fetchResourceList(service, query, limit, offset, room);
const { service, query } = getServiceAndQuery(room)
const response = await fetchResourceList(service, query, limit, offset, room)
console.log(`Fetched ${response.length} message(s) for page ${page}.`);
console.log(`Fetched ${response.length} message(s) for page ${page}.`)
if (handleNoMessagesScenario(isPolling, page, response, messagesContainer)) {
return;
return
}
// Re-establish existing identifiers after preparing container
existingIdentifiers = new Set(
Array.from(messagesContainer.querySelectorAll('.message-item'))
.map(item => item.dataset.identifier)
);
)
let mostRecentMessage = getCurrentMostRecentMessage(room);
let mostRecentMessage = getCurrentMostRecentMessage(room)
const fetchMessages = await fetchAllMessages(response, service, room);
const fetchMessages = await fetchAllMessages(response, service, room)
for (const msg of fetchMessages) {
if (!msg) continue;
storeMessageInMap(msg);
if (!msg) continue
storeMessageInMap(msg)
}
const { firstNewMessageIdentifier, updatedMostRecentMessage } = await renderNewMessages(
@ -703,28 +694,28 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
messagesContainer,
room,
mostRecentMessage
);
)
if (firstNewMessageIdentifier && !isPolling) {
scrollToNewMessages(firstNewMessageIdentifier);
scrollToNewMessages(firstNewMessageIdentifier)
}
if (updatedMostRecentMessage) {
updateLatestMessageIdentifiers(room, updatedMostRecentMessage);
updateLatestMessageIdentifiers(room, updatedMostRecentMessage)
}
handleReplyLogic(fetchMessages);
handleReplyLogic(fetchMessages)
await updatePaginationControls(room, limit);
await updatePaginationControls(room, limit)
} catch (error) {
console.error('Error loading messages from QDN:', error);
console.error('Error loading messages from QDN:', error)
}
};
}
function scrollToMessage(identifier) {
const targetElement = document.querySelector(`.message-item[data-identifier="${identifier}"]`);
const targetElement = document.querySelector(`.message-item[data-identifier="${identifier}"]`)
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
@ -732,37 +723,37 @@ function scrollToMessage(identifier) {
const prepareMessageContainer = (messagesContainer, isPolling) => {
if (!isPolling) {
messagesContainer.innerHTML = "";
existingIdentifiers.clear();
messagesContainer.innerHTML = ""
existingIdentifiers.clear()
}
};
}
const getServiceAndQuery = (room) => {
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST";
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"
const query = (room === "admins")
? `${messageIdentifierPrefix}-${room}-e`
: `${messageIdentifierPrefix}-${room}`;
return { service, query };
};
: `${messageIdentifierPrefix}-${room}`
return { service, query }
}
const fetchResourceList = async (service, query, limit, offset, room) => {
//TODO check
return await searchSimple(service, query, '', limit, offset, room, 'false');
};
return await searchSimple(service, query, '', limit, offset, room, 'false')
}
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>`;
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`
}
return true;
return true
}
return false;
};
return false
}
const getCurrentMostRecentMessage = (room) => {
return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
};
return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null
}
// 1) Convert fetchAllMessages to fully async
const fetchAllMessages = async (response, service, room) => {
@ -771,19 +762,19 @@ const fetchAllMessages = async (response, service, room) => {
const messages = await Promise.all(
response.map(async (resource) => {
try {
const msg = await fetchFullMessage(resource, service, room);
const msg = await fetchFullMessage(resource, service, room)
return msg; // This might be null if you do that check in fetchFullMessage
} catch (err) {
console.error(`Skipping resource ${resource.identifier} due to error:`, err);
console.error(`Skipping resource ${resource.identifier} due to error:`, err)
// Return null so it doesn't break everything
return null;
return null
}
})
);
)
// Filter out any that are null/undefined (missing or errored)
return messages.filter(Boolean);
};
return messages.filter(Boolean)
}
// 2) fetchFullMessage is already async. We keep it async/await-based
@ -792,27 +783,27 @@ const fetchFullMessage = async (resource, service, room) => {
if (messagesById[resource.identifier]) {
// Possibly also check if the local data is "up to date," //TODO when adding 'edit' ability to messages, will also need to verify timestamp in saved data.
// but if you trust your local data, skip the fetch entirely.
console.log(`Skipping fetch. Found in local store: ${resource.identifier}`);
return messagesById[resource.identifier];
console.log(`Skipping fetch. Found in local store: ${resource.identifier}`)
return messagesById[resource.identifier]
}
try {
// Skip if already displayed
if (existingIdentifiers.has(resource.identifier)) {
return null;
return null
}
console.log(`Fetching message with identifier: ${resource.identifier}`);
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);
const timestamp = resource.updated || resource.created
const formattedTimestamp = await timestampToHumanReadableDate(timestamp)
const messageObject = await processMessageObject(messageResponse, room)
const builtMsg = {
name: resource.name,
@ -822,14 +813,14 @@ const fetchFullMessage = async (resource, service, room) => {
replyTo: messageObject?.replyTo || null,
timestamp,
attachments: messageObject?.attachments || [],
};
}
// 3) Store it in the map so we skip future fetches
storeMessageInMap(builtMsg);
storeMessageInMap(builtMsg)
return builtMsg;
return builtMsg
} catch (error) {
console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`);
console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`)
return {
name: resource.name,
content: "<em>Error loading message</em>",
@ -838,14 +829,14 @@ const fetchFullMessage = async (resource, service, room) => {
replyTo: null,
timestamp: resource.updated || resource.created,
attachments: [],
};
}
}
};
}
const fetchReplyData = async (service, name, identifier, room, replyTimestamp) => {
try {
console.log(`Fetching message with identifier: ${identifier}`);
console.log(`Fetching message with identifier: ${identifier}`)
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name,
@ -867,7 +858,7 @@ const fetchReplyData = async (service, name, identifier, room, replyTimestamp) =
replyTo: messageObject?.replyTo || null,
timestamp: replyTimestamp,
attachments: messageObject?.attachments || [],
};
}
} catch (error) {
console.error(`Failed to fetch message ${identifier}: ${error.message}`)
return {
@ -898,41 +889,41 @@ const processMessageObject = async (messageResponse, room) => {
};
const renderNewMessages = async (fetchMessages, existingIdentifiers, messagesContainer, room, mostRecentMessage) => {
let firstNewMessageIdentifier = null;
let updatedMostRecentMessage = mostRecentMessage;
let firstNewMessageIdentifier = null
let updatedMostRecentMessage = mostRecentMessage
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
const isNewMessage = isMessageNew(message, mostRecentMessage);
const isNewMessage = isMessageNew(message, mostRecentMessage)
if (isNewMessage && !firstNewMessageIdentifier) {
firstNewMessageIdentifier = message.identifier;
firstNewMessageIdentifier = message.identifier
}
const messageHTML = await buildMessageHTML(message, fetchMessages, room, isNewMessage);
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
const messageHTML = await 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);
existingIdentifiers.add(message.identifier)
}
}
return { firstNewMessageIdentifier, updatedMostRecentMessage };
};
return { firstNewMessageIdentifier, updatedMostRecentMessage }
}
const isMessageNew = (message, mostRecentMessage) => {
return !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
};
return !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp)
}
const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const replyHtml = await buildReplyHtml(message, room);
const attachmentHtml = await buildAttachmentHtml(message, room);
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
const replyHtml = await buildReplyHtml(message, room)
const attachmentHtml = await buildAttachmentHtml(message, room)
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
return `
<div class="message-item" data-identifier="${message.identifier}">
@ -956,21 +947,21 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const buildReplyHtml = async (message, room) => {
// 1) If no replyTo, skip
if (!message.replyTo) return "";
if (!message.replyTo) return ""
// 2) Decide which QDN service for this room
const replyService = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST";
const replyIdentifier = message.replyTo;
const replyService = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"
const replyIdentifier = message.replyTo
// 3) Check if we already have a *saved* message
const savedRepliedToMessage = messagesById[replyIdentifier];
console.log("savedRepliedToMessage", savedRepliedToMessage);
const savedRepliedToMessage = messagesById[replyIdentifier]
console.log("savedRepliedToMessage", savedRepliedToMessage)
// 4) If we do, try to process/decrypt it
if (savedRepliedToMessage) {
if (savedRepliedToMessage) {
// We successfully processed the cached message
console.log("Using saved message data for reply:", savedRepliedToMessage);
console.log("Using saved message data for reply:", savedRepliedToMessage)
return `
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
<div class="reply-header">
@ -979,32 +970,32 @@ const buildReplyHtml = async (message, room) => {
</div>
<div class="reply-content">${savedRepliedToMessage.content}</div>
</div>
`;
`
} else {
// The cached message is invalid
console.log("Saved message found but processMessageObject returned null. Falling back...");
console.log("Saved message found but processMessageObject returned null. Falling back...")
}
}
// 5) Fallback approach: If we don't have it in memory OR the cached version was invalid
try {
const replyData = await searchSimple(replyService, replyIdentifier, "", 1);
const replyData = await searchSimple(replyService, replyIdentifier, "", 1)
if (!replyData || !replyData.name) {
console.log("No data found via searchSimple. Skipping reply rendering.");
return "";
console.log("No data found via searchSimple. Skipping reply rendering.")
return ""
}
// We'll use replyData to fetch the actual message from QDN
const replyName = replyData.name;
const replyTimestamp = replyData.updated || replyData.created;
console.log("message not found in workable form, using searchSimple result =>", replyData);
const replyName = replyData.name
const replyTimestamp = replyData.updated || replyData.created
console.log("message not found in workable form, using searchSimple result =>", replyData)
// This fetches and decrypts the actual message
const repliedMessage = await fetchReplyData(replyService, replyName, replyIdentifier, room, replyTimestamp);
if (!repliedMessage) return "";
const repliedMessage = await fetchReplyData(replyService, replyName, replyIdentifier, room, replyTimestamp)
if (!repliedMessage) return ""
// Now store the final message in the map for next time
storeMessageInMap(repliedMessage);
storeMessageInMap(repliedMessage)
// Return final HTML
return `
@ -1014,28 +1005,28 @@ const buildReplyHtml = async (message, room) => {
</div>
<div class="reply-content">${repliedMessage.content}</div>
</div>
`;
`
} catch (error) {
throw error;
throw error
}
};
}
const buildAttachmentHtml = async (message, room) => {
if (!message.attachments || message.attachments.length === 0) {
return "";
return ""
}
// Map over attachments -> array of Promises
const attachmentsHtmlPromises = message.attachments.map(attachment =>
buildSingleAttachmentHtml(attachment, room)
);
)
// Wait for all Promises to resolve -> array of HTML strings
const attachmentsHtmlArray = await Promise.all(attachmentsHtmlPromises);
const attachmentsHtmlArray = await Promise.all(attachmentsHtmlPromises)
// Join them into a single string
return attachmentsHtmlArray.join("");
};
return attachmentsHtmlArray.join("")
}
const buildSingleAttachmentHtml = async (attachment, room) => {
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
@ -1052,7 +1043,7 @@ const buildSingleAttachmentHtml = async (attachment, room) => {
(room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
// const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
const decryptedBase64 = await fetchEncryptedImageBase64(attachment.service, attachment.name, attachment.identifier, attachment.mimeType)
const dataUrl = `data:image/png;base64,${decryptedBase64}`
const dataUrl = `data:image/${attachment.mimeType};base64,${decryptedBase64}`
return `
<div class="attachment">
<img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"/>
@ -1060,7 +1051,7 @@ const buildSingleAttachmentHtml = async (attachment, room) => {
Save ${attachment.filename}
</button>
</div>
`;
`
} else {
return `

View File

@ -8,7 +8,7 @@ let isOutsideOfUiDevelopment = false
if (typeof qortalRequest === 'function') {
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
isOutsideOfUiDevelopment = false
baseUrl = ''
baseUrl = ''
} else {
console.log('qortalRequest is not available as a function. Setting baseUrl to localhost.')
isOutsideOfUiDevelopment = true
@ -154,7 +154,6 @@ const getAddressInfo = async (address) => {
method: 'GET',
})
const addressData = await response.json()
console.log(`address data:`,addressData)
return {
address: addressData.address,
@ -256,6 +255,12 @@ const getNameInfo = async (name) => {
console.log('name:', name)
try {
const response = await fetch(`${baseUrl}/names/${name}`)
if (!response.ok) {
console.warn(`Failed to fetch name info for: ${name}, status: ${response.status}`)
return null
}
const data = await response.json()
console.log('Fetched name info:', data)
return {
@ -665,9 +670,11 @@ const searchSimple = async (service, identifier, name, limit = 1500, offset = 0,
if (name && !identifier && !room) {
console.log('name only searchSimple', name)
urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}`
} else if (!name && identifier && !room) {
console.log('identifier only searchSimple', identifier)
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}`
} else if (!name && !identifier && !room) {
console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`)
return null
@ -675,6 +682,7 @@ const searchSimple = async (service, identifier, name, limit = 1500, offset = 0,
} else {
console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}'`)
}
const response = await fetch(`${baseUrl}/arbitrary/resources/searchsimple?${urlSuffix}`, {
method: 'GET',
headers: { 'accept': 'application/json' }
@ -703,10 +711,8 @@ const searchSimple = async (service, identifier, name, limit = 1500, offset = 0,
console.error("error during searchSimple", error)
throw error
}
}
}
const searchAllCountOnly = async (query, room) => {
try {
let offset = 0
@ -719,7 +725,6 @@ const searchAllCountOnly = async (query, room) => {
try {
console.log(`'mintership-forum-message' not found, switching to actual query...`)
if (room === "admins") {
while (hasMore) {
const response = await qortalRequest({
@ -764,7 +769,6 @@ const searchAllCountOnly = async (query, room) => {
}
}
}
return totalCount
} catch (error) {
@ -788,7 +792,6 @@ const searchAllCountOnly = async (query, room) => {
}
}else {
while (hasMore) {
const response = await searchSimple('BLOG_POST', query, '', limit, offset, room, false)
@ -1095,6 +1098,34 @@ const getProductDetails = async (service, name, identifier) => {
// Qortal poll-related calls ----------------------------------------------------------------------
const getPollOwnerAddress = async (pollName) => {
try {
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
})
const pollData = await response.json()
return pollData.owner
} catch (error) {
console.error(`Error fetching poll results for ${pollName}:`, error)
return null
}
}
const getPollPublisherPublicKey = async (pollName) => {
try {
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
})
const pollData = await response.json()
return pollData.creatorPublicKey
} catch (error) {
console.error(`Error fetching poll results for ${pollName}:`, error)
return null
}
}
const fetchPollResults = async (pollName) => {
try {
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
@ -1107,7 +1138,7 @@ const fetchPollResults = async (pollName) => {
console.error(`Error fetching poll results for ${pollName}:`, error)
return null
}
}
}
// Vote YES on a poll ------------------------------
const voteYesOnPoll = async (poll) => {
@ -1116,7 +1147,7 @@ const voteYesOnPoll = async (poll) => {
pollName: poll,
optionIndex: 0,
})
}
}
// Vote NO on a poll -----------------------------
const voteNoOnPoll = async (poll) => {
@ -1125,7 +1156,7 @@ const voteYesOnPoll = async (poll) => {
pollName: poll,
optionIndex: 1,
})
}
}
// export {
// userState,

View File

@ -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.65b<br></a></span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.66b<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>
@ -197,6 +197,23 @@
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.66beta 12-30-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
New fixes for fake names, and not displaying minters that are already minters. Also, fix for QuickMythril 'poll hijack'. Fixed displaying of encrypted images in Minter Room, and more code cleanup.</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
@ -383,7 +400,7 @@
</div>
<a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.65beta</p>
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.66beta</p>
</a>
</div>
<div class="col-12 col-lg-6">