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:
parent
cfccfab99a
commit
5a6baaef66
@ -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;
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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 `
|
||||
|
@ -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,
|
||||
|
21
index.html
21
index.html
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user