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