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

View File

@ -140,7 +140,6 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
const processMinterCards = async (validMinterCards) => { const processMinterCards = async (validMinterCards) => {
const latestCardsMap = new Map() const latestCardsMap = new Map()
// Step 1: Filter and keep the most recent card per identifier
validMinterCards.forEach(card => { validMinterCards.forEach(card => {
const timestamp = card.updated || card.created || 0 const timestamp = card.updated || card.created || 0
const existingCard = latestCardsMap.get(card.identifier) const existingCard = latestCardsMap.get(card.identifier)
@ -150,36 +149,47 @@ const processMinterCards = async (validMinterCards) => {
} }
}) })
// Step 2: Extract unique cards const minterGroupMembers = await fetchMinterGroupMembers()
const uniqueValidCards = Array.from(latestCardsMap.values()) const minterGroupAddresses = minterGroupMembers.map(m => m.member)
// Step 3: Group by minterName and select the most recent card per minterName
const minterNameMap = new Map() const minterNameMap = new Map()
for (const card of validMinterCards) { for (const card of validMinterCards) {
const minterName = await extractMinterCardsMinterName(card.identifier) 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 existingCard = minterNameMap.get(minterName)
const cardTimestamp = card.updated || card.created || 0 const cardTimestamp = card.updated || card.created || 0
const existingTimestamp = existingCard?.updated || existingCard?.created || 0 const existingTimestamp = existingCard?.updated || existingCard?.created || 0
// Keep only the most recent card for each minterName
if (!existingCard || cardTimestamp > existingTimestamp) { if (!existingCard || cardTimestamp > existingTimestamp) {
minterNameMap.set(minterName, card) minterNameMap.set(minterName, card)
} }
} }
// Step 4: Filter cards to ensure each minterName is included only once
const finalCards = [] const finalCards = []
const seenMinterNames = new Set() const seenMinterNames = new Set()
for (const [minterName, card] of minterNameMap.entries()) { for (const [minterName, card] of minterNameMap.entries()) {
if (!seenMinterNames.has(minterName)) { if (!seenMinterNames.has(minterName)) {
finalCards.push(card) 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) => { finalCards.sort((a, b) => {
const timestampA = a.updated || a.created || 0 const timestampA = a.updated || a.created || 0
const timestampB = b.updated || b.created || 0 const timestampB = b.updated || b.created || 0
@ -191,54 +201,41 @@ const processMinterCards = async (validMinterCards) => {
//Main function to load the Minter Cards ---------------------------------------- //Main function to load the Minter Cards ----------------------------------------
const loadCards = async () => { const loadCards = async () => {
const cardsContainer = document.getElementById("cards-container"); const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Loading cards...</p>"; cardsContainer.innerHTML = "<p>Loading cards...</p>"
try { try {
// const response = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// service: "BLOG_POST",
// query: cardIdentifierPrefix,
// mode: "ALL"
// })
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0) const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0)
if (!response || !Array.isArray(response) || response.length === 0) { if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>"; cardsContainer.innerHTML = "<p>No cards found.</p>"
return; return;
} }
// Validate cards and filter // Validate cards and filter
const validatedCards = await Promise.all( const validatedCards = await Promise.all(
response.map(async card => { response.map(async card => {
const isValid = await validateCardStructure(card); const isValid = await validateCardStructure(card)
return isValid ? card : null; return isValid ? card : null
}) })
); );
const validCards = validatedCards.filter(card => card !== null); const validCards = validatedCards.filter(card => card !== null)
if (validCards.length === 0) { if (validCards.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>"; cardsContainer.innerHTML = "<p>No valid cards found.</p>"
return; return
} }
const finalCards = await processMinterCards(validCards) 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 // Display skeleton cards immediately
cardsContainer.innerHTML = ""; cardsContainer.innerHTML = ""
finalCards.forEach(card => { finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier); const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML); cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
}); })
// Fetch and update each card // Fetch and update each card
finalCards.forEach(async card => { finalCards.forEach(async card => {
@ -248,57 +245,62 @@ const loadCards = async () => {
name: card.name, name: card.name,
service: "BLOG_POST", service: "BLOG_POST",
identifier: card.identifier, identifier: card.identifier,
}); })
if (!cardDataResponse) { if (!cardDataResponse) {
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`); console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
removeSkeleton(card.identifier); removeSkeleton(card.identifier)
return; return
} }
// Skip cards without polls
if (!cardDataResponse.poll) { if (!cardDataResponse.poll) {
console.warn(`Skipping card with no poll: ${card.identifier}`); console.warn(`Skipping card with no poll: ${card.identifier}`)
removeSkeleton(card.identifier); removeSkeleton(card.identifier)
return; return
} }
// Fetch poll results const pollPublisherPublicKey = await getPollPublisherPublicKey(cardDataResponse.poll)
const pollResults = await fetchPollResults(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) const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML
const commentCount = await countComments(card.identifier) const commentCount = await countComments(card.identifier)
const cardUpdatedTime = card.updated || null 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) { } catch (error) {
console.error(`Error processing card ${card.identifier}:`, error); console.error(`Error processing card ${card.identifier}:`, error)
removeSkeleton(card.identifier); // Silently remove skeleton on error removeSkeleton(card.identifier)
} }
}); })
} catch (error) { } catch (error) {
console.error("Error loading cards:", error); console.error("Error loading cards:", error)
cardsContainer.innerHTML = "<p>Failed to load cards.</p>"; cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
} }
}; }
const removeSkeleton = (cardIdentifier) => { const removeSkeleton = (cardIdentifier) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`); const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) { if (skeletonCard) {
skeletonCard.remove(); // Remove the skeleton silently skeletonCard.remove()
} }
}; }
const replaceSkeleton = (cardIdentifier, htmlContent) => { const replaceSkeleton = (cardIdentifier, htmlContent) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`); const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) { if (skeletonCard) {
skeletonCard.outerHTML = htmlContent; skeletonCard.outerHTML = htmlContent
} }
}; }
// Function to create a skeleton card
const createSkeletonCardHTML = (cardIdentifier) => { const createSkeletonCardHTML = (cardIdentifier) => {
return ` return `
<div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;"> <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 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>
</div> </div>
`; `
}; }
// Function to check and fech an existing Minter Card if attempting to publish twice ---------------------------------------- // Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
const fetchExistingCard = async () => { const fetchExistingCard = async () => {
try { 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) const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, `${userState.accountName}`, 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) { if (!response || !Array.isArray(response) || response.length === 0) {
console.log("No cards found for the current user.") console.log("No cards found for the current user.")
return null return null
@ -343,7 +333,6 @@ const fetchExistingCard = async () => {
return response[0] return response[0]
} }
// Validate cards asynchronously, check that they are not comments, etc.
const validatedCards = await Promise.all( const validatedCards = await Promise.all(
response.map(async card => { response.map(async card => {
const isValid = await validateCardStructure(card) const isValid = await validateCardStructure(card)
@ -351,14 +340,12 @@ const fetchExistingCard = async () => {
}) })
) )
// Filter out invalid cards
const validCards = validatedCards.filter(card => card !== null) const validCards = validatedCards.filter(card => card !== null)
if (validCards.length > 0) { if (validCards.length > 0) {
// 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]
// Fetch full card data
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: userState.accountName, // User's account name name: userState.accountName, // User's account name
@ -413,17 +400,16 @@ const loadCardIntoForm = async (cardData) => {
// Main function to publish a new Minter Card ----------------------------------------------- // Main function to publish a new Minter Card -----------------------------------------------
const publishCard = async () => { const publishCard = async () => {
const minterGroupData = await fetchMinterGroupMembers(); const minterGroupData = await fetchMinterGroupMembers()
const minterGroupAddresses = minterGroupData.map(m => m.member); // array of addresses const minterGroupAddresses = minterGroupData.map(m => m.member)
// 2) check if user is a minter
const userAddress = userState.accountAddress; const userAddress = userState.accountAddress;
if (minterGroupAddresses.includes(userAddress)) { 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; return;
} }
const header = document.getElementById("card-header").value.trim(); const header = document.getElementById("card-header").value.trim()
const content = document.getElementById("card-content").value.trim(); const content = document.getElementById("card-content").value.trim()
const links = Array.from(document.querySelectorAll(".card-link")) const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim()) .map(input => input.value.trim())
.filter(link => link.startsWith("qortal://")) .filter(link => link.startsWith("qortal://"))
@ -776,7 +762,7 @@ const toggleComments = async (cardIdentifier) => {
if (isHidden) { if (isHidden) {
// Show comments // Show comments
commentButton.textContent = "LOADING..." commentButton.textContent = "LOADING..."
await displayComments(cardIdentifier); await displayComments(cardIdentifier)
commentsSection.style.display = 'block' commentsSection.style.display = 'block'
// Change the button text to 'HIDE COMMENTS' // Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS' commentButton.textContent = 'HIDE COMMENTS'

View File

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

View File

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

View File

@ -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.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> </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>
@ -197,6 +197,23 @@
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6"> <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="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-7 card"> <div class="col-12 col-lg-7 card">
@ -383,7 +400,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.65beta</p> <p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.66beta</p>
</a> </a>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">