Will also be adding additional use cases for the Admin board, and maybe a 'community board' as I really like the board concept for things like decision-making and community managmenent. I also have a really good idea for giving information on the boards via link modal concept. TBD. Will likely make the identifier changes and push announcements out on Monday. IF not sometime this weekend if time allows.
811 lines
32 KiB
JavaScript
811 lines
32 KiB
JavaScript
// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
|
|
const isEncryptedTestMode = true
|
|
const encryptedCardIdentifierPrefix = "test-MDC"
|
|
let isExistingEncryptedCard = false
|
|
let existingDecryptedCardData = {}
|
|
let existingEncryptedCardIdentifier = {}
|
|
let cardMinterName = {}
|
|
let existingCardMinterNames = []
|
|
|
|
console.log("Attempting to load AdminBoard.js");
|
|
|
|
const loadAdminBoardPage = async () => {
|
|
// Clear existing content on the page
|
|
const bodyChildren = document.body.children;
|
|
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
|
const child = bodyChildren[i];
|
|
if (!child.classList.contains("menu")) {
|
|
child.remove();
|
|
}
|
|
}
|
|
|
|
// Add the "Minter Board" content
|
|
const mainContent = document.createElement("div");
|
|
mainContent.innerHTML = `
|
|
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
|
<h1 style="color: lightblue;">AdminBoard</h1>
|
|
<p style="font-size: 1.25em;"> The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins. Any Admin may publish a card, and related data, make comments on existing cards, and vote on existing card data in support or not of the name on the card. It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding/removing from the minter group. </p>
|
|
<p> More functionality will be added over time. One of the first features will be the ability to output the existing card data 'decisions', to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed, and the MINTER group is transferred to 'null'.</p>
|
|
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button>
|
|
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
|
|
<div id="encrypted-cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
|
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
|
<form id="publish-card-form">
|
|
<h3>Create or Update Your Minter Card</h3>
|
|
<label for="minter-name-input">Minter Name:</label>
|
|
<input type="text" id="minter-name-input" maxlength="100" placeholder="Enter Minter's Name" required>
|
|
<label for="card-header">Header:</label>
|
|
<input type="text" id="card-header" maxlength="100" placeholder="Explain main point/issue" required>
|
|
<label for="card-content">Content:</label>
|
|
<textarea id="card-content" placeholder="Enter any information you like...You may also attach links to more in-depth information, etc." required></textarea>
|
|
<label for="card-links">Links (qortal://...):</label>
|
|
<div id="links-container">
|
|
<input type="text" class="card-link" placeholder="Enter QDN link">
|
|
</div>
|
|
<button type="button" id="add-link-button">Add Another Link</button>
|
|
<button type="submit" id="submit-publish-button">Publish Card</button>
|
|
<button type="button" id="cancel-publish-button">Cancel</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(mainContent);
|
|
const publishCardButton = document.getElementById("publish-card-button")
|
|
if (publishCardButton) {
|
|
publishCardButton.addEventListener("click", async () => {
|
|
const publishCardView = document.getElementById("publish-card-view")
|
|
publishCardView.style.display = "flex"
|
|
document.getElementById("encrypted-cards-container").style.display = "none"
|
|
})
|
|
}
|
|
const refreshCardsButton = document.getElementById("refresh-cards-button")
|
|
if (refreshCardsButton) {
|
|
refreshCardsButton.addEventListener("click", async () => {
|
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
|
encryptedCardsContainer.innerHTML = "<p>Refreshing cards...</p>"
|
|
await fetchAllEncryptedCards()
|
|
})
|
|
}
|
|
|
|
const cancelPublishButton = document.getElementById("cancel-publish-button")
|
|
if (cancelPublishButton) {
|
|
cancelPublishButton.addEventListener("click", async () => {
|
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
|
encryptedCardsContainer.style.display = "flex"; // Restore visibility
|
|
const publishCardView = document.getElementById("publish-card-view")
|
|
publishCardView.style.display = "none"; // Hide the publish form
|
|
})
|
|
}
|
|
const addLinkButton = document.getElementById("add-link-button")
|
|
if (addLinkButton) {
|
|
addLinkButton.addEventListener("click", async () => {
|
|
const linksContainer = document.getElementById("links-container")
|
|
const newLinkInput = document.createElement("input")
|
|
newLinkInput.type = "text"
|
|
newLinkInput.className = "card-link"
|
|
newLinkInput.placeholder = "Enter QDN link"
|
|
linksContainer.appendChild(newLinkInput)
|
|
})
|
|
}
|
|
|
|
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
await publishEncryptedCard();
|
|
});
|
|
|
|
// await fetchAndValidateAllAdminCards();
|
|
await fetchAllEncryptedCards();
|
|
}
|
|
|
|
const extractCardsMinterName = (cardIdentifier) => {
|
|
// Ensure the identifier starts with the prefix
|
|
if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) {
|
|
throw new Error('Invalid identifier format or prefix mismatch');
|
|
}
|
|
// Split the identifier into parts
|
|
const parts = cardIdentifier.split('-');
|
|
// Ensure the format has at least 3 parts
|
|
if (parts.length < 3) {
|
|
throw new Error('Invalid identifier format');
|
|
}
|
|
// Extract minterName (everything from the second part to the second-to-last part)
|
|
const minterName = parts.slice(2, -1).join('-');
|
|
// Return the extracted minterName
|
|
return minterName;
|
|
}
|
|
|
|
const processCards = async (validEncryptedCards) => {
|
|
const latestCardsMap = new Map()
|
|
|
|
// Step 1: Filter and keep the most recent card per identifier
|
|
validEncryptedCards.forEach(card => {
|
|
const timestamp = card.updated || card.created || 0
|
|
const existingCard = latestCardsMap.get(card.identifier)
|
|
|
|
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
|
|
latestCardsMap.set(card.identifier, card)
|
|
}
|
|
})
|
|
|
|
// Step 2: Extract unique cards
|
|
const uniqueValidCards = Array.from(latestCardsMap.values())
|
|
|
|
// Step 3: Group by minterName and select the most recent card per minterName
|
|
const minterNameMap = new Map()
|
|
|
|
for (const card of validEncryptedCards) {
|
|
const minterName = await extractCardsMinterName(card.identifier)
|
|
const existingCard = minterNameMap.get(minterName)
|
|
const cardTimestamp = card.updated || card.created || 0
|
|
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
|
|
|
|
if (!existingCardMinterNames.includes(minterName)) {
|
|
existingCardMinterNames.push(minterName)
|
|
console.log(`cardsMinterName: ${minterName} - added to list`)
|
|
}
|
|
|
|
// Keep only the most recent card for each minterName
|
|
if (!existingCard || cardTimestamp > existingTimestamp) {
|
|
minterNameMap.set(minterName, card)
|
|
}
|
|
}
|
|
|
|
// Step 4: Filter cards to ensure each minterName is included only once
|
|
const finalCards = []
|
|
const seenMinterNames = new Set()
|
|
|
|
for (const [minterName, card] of minterNameMap.entries()) {
|
|
if (!seenMinterNames.has(minterName)) {
|
|
finalCards.push(card)
|
|
seenMinterNames.add(minterName) // Mark the minterName as seen
|
|
}
|
|
}
|
|
|
|
// Step 5: Sort by the most recent timestamp
|
|
finalCards.sort((a, b) => {
|
|
const timestampA = a.updated || a.created || 0
|
|
const timestampB = b.updated || b.created || 0
|
|
return timestampB - timestampA
|
|
})
|
|
|
|
return finalCards
|
|
}
|
|
|
|
|
|
//Main function to load the Minter Cards ----------------------------------------
|
|
const fetchAllEncryptedCards = async () => {
|
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container");
|
|
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>";
|
|
|
|
try {
|
|
const response = await qortalRequest({
|
|
action: "SEARCH_QDN_RESOURCES",
|
|
service: "MAIL_PRIVATE",
|
|
query: encryptedCardIdentifierPrefix,
|
|
mode: "ALL"
|
|
});
|
|
|
|
if (!response || !Array.isArray(response) || response.length === 0) {
|
|
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>";
|
|
return;
|
|
}
|
|
|
|
// Validate cards and filter
|
|
const validatedEncryptedCards = await Promise.all(
|
|
response.map(async card => {
|
|
const isValid = await validateEncryptedCardIdentifier(card);
|
|
return isValid ? card : null;
|
|
})
|
|
);
|
|
|
|
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
|
|
|
|
if (validEncryptedCards.length === 0) {
|
|
encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>";
|
|
return;
|
|
}
|
|
const finalCards = await processCards(validEncryptedCards)
|
|
|
|
// Display skeleton cards immediately
|
|
encryptedCardsContainer.innerHTML = "";
|
|
finalCards.forEach(card => {
|
|
const skeletonHTML = createSkeletonCardHTML(card.identifier);
|
|
encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
|
|
});
|
|
|
|
// Fetch and update each card
|
|
finalCards.forEach(async card => {
|
|
try {
|
|
const cardDataResponse = await qortalRequest({
|
|
action: "FETCH_QDN_RESOURCE",
|
|
name: card.name,
|
|
service: "MAIL_PRIVATE",
|
|
identifier: card.identifier,
|
|
encoding: "base64"
|
|
});
|
|
|
|
if (!cardDataResponse) {
|
|
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`);
|
|
removeSkeleton(card.identifier);
|
|
return;
|
|
}
|
|
|
|
const decryptedCardData = await decryptAndParseObject(cardDataResponse);
|
|
|
|
// Skip cards without polls
|
|
if (!decryptedCardData.poll) {
|
|
console.warn(`Skipping card with no poll: ${card.identifier}`);
|
|
removeSkeleton(card.identifier);
|
|
return;
|
|
}
|
|
|
|
// Fetch poll results
|
|
const pollResults = await fetchPollResults(decryptedCardData.poll);
|
|
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
|
|
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
|
|
// Generate final card HTML
|
|
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount);
|
|
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
|
|
} catch (error) {
|
|
console.error(`Error processing card ${card.identifier}:`, error);
|
|
removeEncryptedSkeleton(card.identifier); // Silently remove skeleton on error
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("Error loading cards:", error);
|
|
encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
|
}
|
|
};
|
|
|
|
|
|
const removeEncryptedSkeleton = (cardIdentifier) => {
|
|
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
|
|
if (encryptedSkeletonCard) {
|
|
encryptedSkeletonCard.remove(); // Remove the skeleton silently
|
|
}
|
|
};
|
|
|
|
const replaceEncryptedSkeleton = (cardIdentifier, htmlContent) => {
|
|
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
|
|
if (encryptedSkeletonCard) {
|
|
encryptedSkeletonCard.outerHTML = htmlContent;
|
|
}
|
|
};
|
|
|
|
// Function to create a skeleton card
|
|
const createEncryptedSkeletonCardHTML = (cardIdentifier) => {
|
|
return `
|
|
<div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;">
|
|
<div style="display: flex; align-items: center;">
|
|
<div style="width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;"></div>
|
|
<div style="margin-left: 10px;">
|
|
<div style="width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;"></div>
|
|
<div style="width: 80px; height: 15px; background-color: #ddd;"></div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 10px;">
|
|
<div style="width: 100%; height: 40px; background-color: #eee;"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
|
|
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
|
|
const fetchExistingEncryptedCard = async (minterName) => {
|
|
try {
|
|
// Step 1: Perform the search
|
|
const response = await qortalRequest({
|
|
action: "SEARCH_QDN_RESOURCES",
|
|
service: "MAIL_PRIVATE",
|
|
identifier: encryptedCardIdentifierPrefix,
|
|
query: minterName,
|
|
mode: "ALL",
|
|
});
|
|
|
|
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
|
|
|
|
// Step 2: Check if the response is an array and not empty
|
|
if (!response || !Array.isArray(response) || response.length === 0) {
|
|
console.log("No cards found for the current user.");
|
|
return null;
|
|
}
|
|
|
|
// Step 3: Validate cards asynchronously
|
|
const validatedCards = await Promise.all(
|
|
response.map(async card => {
|
|
const isValid = await validateEncryptedCardIdentifier(card)
|
|
return isValid ? card : null;
|
|
})
|
|
);
|
|
|
|
// Step 4: Filter out invalid cards
|
|
const validCards = validatedCards.filter(card => card !== null);
|
|
|
|
if (validCards.length > 0) {
|
|
// Step 5: Sort by most recent timestamp
|
|
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0];
|
|
|
|
// Step 6: Fetch full card data
|
|
const cardDataResponse = await qortalRequest({
|
|
action: "FETCH_QDN_RESOURCE",
|
|
name: mostRecentCard.name,
|
|
service: mostRecentCard.service,
|
|
identifier: mostRecentCard.identifier,
|
|
encoding: "base64"
|
|
});
|
|
|
|
existingEncryptedCardIdentifier = mostRecentCard.identifier;
|
|
|
|
existingDecryptedCardData = await decryptAndParseObject(cardDataResponse)
|
|
console.log("Full card data fetched successfully:", existingDecryptedCardData);
|
|
|
|
return existingDecryptedCardData;
|
|
}
|
|
|
|
console.log("No valid cards found.");
|
|
return null;
|
|
} catch (error) {
|
|
console.error("Error fetching existing card:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Validate that a card is indeed a card and not a comment. -------------------------------------
|
|
const validateEncryptedCardIdentifier = async (card) => {
|
|
return (
|
|
typeof card === "object" &&
|
|
card.name &&
|
|
card.service === "MAIL_PRIVATE" &&
|
|
card.identifier && !card.identifier.includes("comment") &&
|
|
card.created
|
|
);
|
|
}
|
|
|
|
// Load existing card data passed, into the form for editing -------------------------------------
|
|
const loadEncryptedCardIntoForm = async () => {
|
|
if (existingDecryptedCardData) {
|
|
console.log("Loading existing card data:", existingDecryptedCardData);
|
|
document.getElementById("minter-name-input").value = existingDecryptedCardData.minterName
|
|
document.getElementById("card-header").value = existingDecryptedCardData.header
|
|
document.getElementById("card-content").value = existingDecryptedCardData.content
|
|
|
|
const linksContainer = document.getElementById("links-container");
|
|
linksContainer.innerHTML = ""; // Clear previous links
|
|
existingDecryptedCardData.links.forEach(link => {
|
|
const linkInput = document.createElement("input");
|
|
linkInput.type = "text";
|
|
linkInput.className = "card-link";
|
|
linkInput.value = link;
|
|
linksContainer.appendChild(linkInput);
|
|
});
|
|
}
|
|
}
|
|
|
|
const validateMinterName = async(minterName) => {
|
|
try {
|
|
const nameInfo = await getNameInfo(minterName)
|
|
if (!nameInfo) {
|
|
return error (`No NameInfo able to be obtained? Did you pass name?`)
|
|
}
|
|
const name = nameInfo.name
|
|
return name
|
|
} catch (error){
|
|
console.error(`extracting name from name info: ${minterName} failed.`, error)
|
|
}
|
|
}
|
|
|
|
// Main function to publish a new Minter Card -----------------------------------------------
|
|
const publishEncryptedCard = async () => {
|
|
const minterNameInput = document.getElementById("minter-name-input").value.trim();
|
|
const header = document.getElementById("card-header").value.trim();
|
|
const content = document.getElementById("card-content").value.trim();
|
|
const links = Array.from(document.querySelectorAll(".card-link"))
|
|
.map(input => input.value.trim())
|
|
.filter(link => link.startsWith("qortal://"));
|
|
const publishedMinterName = await validateMinterName(minterNameInput)
|
|
|
|
if (!header || !content) {
|
|
alert("Header and Content are required!");
|
|
return;
|
|
}
|
|
|
|
if (!publishedMinterName) {
|
|
alert(`Minter name invalid! Name input: ${minterNameInput} - please check the name and try again!`)
|
|
return;
|
|
}
|
|
|
|
if (!isExistingEncryptedCard) {
|
|
if (existingCardMinterNames.includes(publishedMinterName)) {
|
|
const updateCard = confirm(`Minter Name: ${publishedMinterName} - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... `)
|
|
if (updateCard) {
|
|
await fetchExistingEncryptedCard(publishedMinterName)
|
|
await loadEncryptedCardIntoForm()
|
|
isExistingEncryptedCard = true
|
|
return
|
|
}else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`;
|
|
const pollName = `${cardIdentifier}-poll`;
|
|
const pollDescription = `Admin Board Poll Published By ${userState.accountName}`;
|
|
|
|
const cardData = {
|
|
minterName: `${publishedMinterName}`,
|
|
header,
|
|
content,
|
|
links,
|
|
creator: userState.accountName,
|
|
timestamp: Date.now(),
|
|
poll: pollName,
|
|
};
|
|
|
|
try {
|
|
|
|
let base64CardData = await objectToBase64(cardData);
|
|
if (!base64CardData) {
|
|
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
|
base64CardData = btoa(JSON.stringify(cardData));
|
|
}
|
|
|
|
|
|
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
|
adminPublicKeys = verifiedAdminPublicKeys
|
|
|
|
await qortalRequest({
|
|
action: "PUBLISH_QDN_RESOURCE",
|
|
name: userState.accountName,
|
|
service: "MAIL_PRIVATE",
|
|
identifier: cardIdentifier,
|
|
data64: base64CardData,
|
|
encrypt: true,
|
|
publicKeys: verifiedAdminPublicKeys
|
|
});
|
|
|
|
if (!isExistingEncryptedCard){
|
|
await qortalRequest({
|
|
action: "CREATE_POLL",
|
|
pollName,
|
|
pollDescription,
|
|
pollOptions: ['Yes, No'],
|
|
pollOwnerAddress: userState.accountAddress,
|
|
});
|
|
|
|
alert("Card and poll published successfully!");
|
|
existingCardMinterNames.push(`${publishedMinterName}`)
|
|
}
|
|
|
|
if (isExistingEncryptedCard){
|
|
alert("Card Updated Successfully! (No poll updates are possible at this time...)")
|
|
}
|
|
document.getElementById("publish-card-form").reset();
|
|
document.getElementById("publish-card-view").style.display = "none";
|
|
document.getElementById("encrypted-cards-container").style.display = "flex";
|
|
} catch (error) {
|
|
console.error("Error publishing card or poll:", error);
|
|
alert("Failed to publish card and poll.");
|
|
}
|
|
}
|
|
|
|
const getEncryptedCommentCount = async (cardIdentifier) => {
|
|
try {
|
|
const response = await qortalRequest({
|
|
action: 'SEARCH_QDN_RESOURCES',
|
|
service: 'MAIL_PRIVATE',
|
|
query: `comment-${cardIdentifier}`,
|
|
mode: "ALL"
|
|
});
|
|
// Just return the count; no need to decrypt each comment here
|
|
return Array.isArray(response) ? response.length : 0;
|
|
} catch (error) {
|
|
console.error(`Error fetching comment count for ${cardIdentifier}:`, error);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
// Post a comment on a card. ---------------------------------
|
|
const postEncryptedComment = async (cardIdentifier) => {
|
|
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
|
const commentText = commentInput.value.trim();
|
|
if (!commentText) {
|
|
alert('Comment cannot be empty!');
|
|
return;
|
|
}
|
|
|
|
const postTimestamp = Date.now()
|
|
console.log(`timestmp to be posted: ${postTimestamp}`)
|
|
|
|
const commentData = {
|
|
content: commentText,
|
|
creator: userState.accountName,
|
|
timestamp: postTimestamp,
|
|
};
|
|
|
|
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
|
|
|
|
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) {
|
|
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
|
adminPublicKeys = verifiedAdminPublicKeys
|
|
}
|
|
|
|
|
|
try {
|
|
const base64CommentData = await objectToBase64(commentData);
|
|
if (!base64CommentData) {
|
|
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
|
base64CommentData = btoa(JSON.stringify(commentData));
|
|
}
|
|
|
|
await qortalRequest({
|
|
action: "PUBLISH_QDN_RESOURCE",
|
|
name: userState.accountName,
|
|
service: "MAIL_PRIVATE",
|
|
identifier: commentIdentifier,
|
|
data64: base64CommentData,
|
|
encrypt: true,
|
|
publicKeys: adminPublicKeys
|
|
});
|
|
|
|
alert('Comment posted successfully!');
|
|
commentInput.value = ''; // Clear input
|
|
} catch (error) {
|
|
console.error('Error posting comment:', error);
|
|
alert('Failed to post comment.');
|
|
}
|
|
};
|
|
|
|
//Fetch the comments for a card with passed card identifier ----------------------------
|
|
const fetchEncryptedComments = async (cardIdentifier) => {
|
|
try {
|
|
const response = await qortalRequest({
|
|
action: 'SEARCH_QDN_RESOURCES',
|
|
service: 'MAIL_PRIVATE',
|
|
query: `comment-${cardIdentifier}`,
|
|
mode: "ALL"
|
|
});
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// display the comments on the card, with passed cardIdentifier to identify the card --------------
|
|
const displayEncryptedComments = async (cardIdentifier) => {
|
|
try {
|
|
const comments = await fetchEncryptedComments(cardIdentifier);
|
|
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
|
|
|
// Fetch and display each comment
|
|
for (const comment of comments) {
|
|
const commentDataResponse = await qortalRequest({
|
|
action: "FETCH_QDN_RESOURCE",
|
|
name: comment.name,
|
|
service: "MAIL_PRIVATE",
|
|
identifier: comment.identifier,
|
|
encoding: "base64"
|
|
});
|
|
|
|
const decryptedCommentData = await decryptAndParseObject(commentDataResponse)
|
|
|
|
const timestampCheck = comment.updated || comment.created || 0
|
|
const timestamp = await timestampToHumanReadableDate(timestampCheck);
|
|
|
|
//TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section.
|
|
const commentHTML = `
|
|
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
|
|
<p><strong><u>${decryptedCommentData.creator}</strong>:</p></u>
|
|
<p>${decryptedCommentData.content}</p>
|
|
<p><i>${timestamp}</p></i>
|
|
</div>
|
|
`;
|
|
commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error displaying comments for ${cardIdentifier}:`, error);
|
|
alert("Failed to load comments. Please try again.");
|
|
}
|
|
};
|
|
|
|
const calculateAdminBoardPollResults = async (pollData, minterGroupMembers, minterAdmins) => {
|
|
const memberAddresses = minterGroupMembers.map(member => member.member)
|
|
const minterAdminAddresses = minterAdmins.map(member => member.member)
|
|
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
|
|
const groupAdminAddresses = adminGroupsMembers.map(member => member.member)
|
|
const adminAddresses = [];
|
|
adminAddresses.push(...minterAdminAddresses,...groupAdminAddresses);
|
|
|
|
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, yesWeight = 0 , noWeight = 0
|
|
|
|
pollData.voteWeights.forEach(weightData => {
|
|
if (weightData.optionName === 'Yes') {
|
|
yesWeight = weightData.voteWeight
|
|
} else if (weightData.optionName === 'No') {
|
|
noWeight = weightData.voteWeight
|
|
}
|
|
})
|
|
|
|
for (const vote of pollData.votes) {
|
|
const voterAddress = await getAddressFromPublicKey(vote.voterPublicKey)
|
|
console.log(`voter address: ${voterAddress}`)
|
|
|
|
if (vote.optionIndex === 0) {
|
|
adminAddresses.includes(voterAddress) ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`)
|
|
} else if (vote.optionIndex === 1) {
|
|
adminAddresses.includes(voterAddress) ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`)
|
|
}
|
|
}
|
|
|
|
// TODO - create a new function to calculate the weights of each voting MINTER only.
|
|
// This will give ALL weight whether voter is in minter group or not...
|
|
// until that is changed on the core we must calculate manually.
|
|
const totalYesWeight = yesWeight
|
|
const totalNoWeight = noWeight
|
|
|
|
const totalYes = adminYes + minterYes
|
|
const totalNo = adminNo + minterNo
|
|
|
|
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo, totalYesWeight, totalNoWeight }
|
|
}
|
|
|
|
const toggleEncryptedComments = async (cardIdentifier) => {
|
|
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
|
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
|
await displayEncryptedComments(cardIdentifier);
|
|
commentsSection.style.display = 'block';
|
|
} else {
|
|
commentsSection.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
const createLinkDisplayModal = async () => {
|
|
const modalHTML = `
|
|
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;">
|
|
<div style="position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;">
|
|
<iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
<button onclick="closeLinkDisplayModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
}
|
|
|
|
// Function to open the modal
|
|
const openLinkDisplayModal = async (link) => {
|
|
const processedLink = await processQortalLinkForRendering(link) // Process the link to replace `qortal://` for rendering in modal
|
|
const modal = document.getElementById('modal');
|
|
const modalContent = document.getElementById('modalContent');
|
|
modalContent.src = processedLink; // Set the iframe source to the link
|
|
modal.style.display = 'block'; // Show the modal
|
|
}
|
|
|
|
// Function to close the modal
|
|
const closeLinkDisplayModal = async () => {
|
|
const modal = document.getElementById('modal');
|
|
const modalContent = document.getElementById('modalContent');
|
|
modal.style.display = 'none'; // Hide the modal
|
|
modalContent.src = ''; // Clear the iframe source
|
|
}
|
|
|
|
// const processQortalLinkForRendering = async (link) => {
|
|
// if (link.startsWith('qortal://')) {
|
|
// const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
|
|
// if (match) {
|
|
// const firstParam = match[1].toUpperCase(); // Convert to uppercase
|
|
// const remainingPath = match[2] || ""; // Rest of the URL
|
|
// // Perform any asynchronous operation if necessary
|
|
// await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
|
|
// return `/render/${firstParam}${remainingPath}`;
|
|
// }
|
|
// }
|
|
// return link; // Return unchanged if not a Qortal link
|
|
// }
|
|
|
|
const processQortalLinkForRendering = async (link) => {
|
|
if (link.startsWith('qortal://')) {
|
|
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
|
|
if (match) {
|
|
const firstParam = match[1].toUpperCase();
|
|
const remainingPath = match[2] || "";
|
|
const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined
|
|
|
|
// Simulating async operation if needed
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
// Append theme as a query parameter
|
|
return `/render/${firstParam}${remainingPath}?theme=${themeColor}`;
|
|
}
|
|
}
|
|
return link;
|
|
};
|
|
|
|
async function getMinterAvatar(minterName) {
|
|
const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
|
|
|
|
try {
|
|
const response = await fetch(avatarUrl, { method: 'HEAD' });
|
|
if (response.ok) {
|
|
// Avatar exists, return the image HTML
|
|
return `<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">`;
|
|
} else {
|
|
// Avatar not found or no permission
|
|
return '';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking avatar availability:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Create the overall Minter Card HTML -----------------------------------------------
|
|
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
|
const { minterName, header, content, links, creator, timestamp, poll } = cardData;
|
|
const formattedDate = new Date(timestamp).toLocaleString();
|
|
const minterAvatar = await getMinterAvatar(minterName)
|
|
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
|
|
const creatorAvatar = await getMinterAvatar(creator)
|
|
const linksHTML = links.map((link, index) => `
|
|
<button onclick="openLinkDisplayModal('${link}')">
|
|
${`Link ${index + 1} - ${link}`}
|
|
</button>
|
|
`).join("");
|
|
|
|
const minterGroupMembers = await fetchMinterGroupMembers();
|
|
const minterAdmins = await fetchMinterGroupAdmins();
|
|
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculateAdminBoardPollResults(pollResults, minterGroupMembers, minterAdmins)
|
|
await createModal()
|
|
return `
|
|
<div class="admin-card">
|
|
<div class="minter-card-header">
|
|
<h2 class="support-header"> Created By: </h2>
|
|
${creatorAvatar}
|
|
<h2>${creator}</h2>
|
|
<div class="support-header"><h5> REGARDING: </h5></div>
|
|
${minterAvatar}
|
|
<h3>${minterName}</h3>
|
|
<p>${header}</p>
|
|
</div>
|
|
<div class="info">
|
|
${content}
|
|
</div>
|
|
<div class="support-header"><h5>LINKS</h5></div>
|
|
<div class="info-links">
|
|
${linksHTML}
|
|
</div>
|
|
<div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
|
|
<div class="minter-card-results">
|
|
<div class="admin-results">
|
|
<span class="admin-yes">Admin Support: ${adminYes}</span>
|
|
<span class="admin-no">Admin Against: ${adminNo}</span>
|
|
</div>
|
|
<div class="minter-results">
|
|
<span class="minter-yes">Supporting Weight ${totalYesWeight}</span>
|
|
<span class="minter-no">Denial Weight ${totalNoWeight}</span>
|
|
</div>
|
|
</div>
|
|
<div class="support-header"><h5>SUPPORT or DENY</h5><h5 style="color: #ffae42;">${minterName}</h5>
|
|
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
|
|
</div>
|
|
<div class="actions">
|
|
<div class="actions-buttons">
|
|
<button class="yes" onclick="voteYesOnPoll('${poll}')">SUPPORT</button>
|
|
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
|
|
<button class="no" onclick="voteNoOnPoll('${poll}')">OPPOSE</button>
|
|
</div>
|
|
</div>
|
|
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
|
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
|
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
|
<button onclick="postEncryptedComment('${cardIdentifier}')">Post Comment</button>
|
|
</div>
|
|
<p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|