const isEncryptedTestMode = false const encryptedCardIdentifierPrefix = "card-MAC" let isUpdateCard = false let existingDecryptedCardData = {} let existingEncryptedCardIdentifier = {} let cardMinterName = {} let existingCardMinterNames = [] let isTopic = false let attemptLoadAdminDataCount = 0 let adminMemberCount = 0 let adminPublicKeys = [] let kickTransactions = [] let banTransactions = [] let adminBoardState = { kickedCards: new Set(), // store identifiers bannedCards: new Set(), // likewise hiddenList: new Set(), // user-hidden // ... we can add other things to state if needed... } const loadAdminBoardState = () => { // Load from localStorage if available const rawState = localStorage.getItem('adminBoardState') if (rawState) { try { const parsed = JSON.parse(rawState); // Make sure bannedCards and kickedCards are sets return { bannedCards: new Set(parsed.bannedCards ?? []), kickedCards: new Set(parsed.kickedCards ?? []), hiddenList: new Set(parsed.hiddenList ?? []), // ... any other fields }; } catch (e) { console.warn("Failed to parse adminBoardState from storage:", e) } } // If nothing found or parse error, return a default return { bannedCards: new Set(), kickedCards: new Set(), hiddenList: new Set(), } } // Saving the state back into localStorage as needed: const saveAdminBoardState = () => { const stateToSave = { bannedCards: Array.from(adminBoardState.bannedCards), kickedCards: Array.from(adminBoardState.kickedCards), hiddenList: Array.from(adminBoardState.hiddenList), } localStorage.setItem('adminBoardState', JSON.stringify(stateToSave)) } 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 = `

AdminBoard

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.

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'.

` 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 = "

Refreshing cards...

" await fetchAllEncryptedCards(true) }) } 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) }) } const showKickedBannedCheckbox = document.getElementById('admin-show-kicked-banned-checkbox') if (showKickedBannedCheckbox) { showKickedBannedCheckbox.addEventListener('change', async (event) => { await fetchAllEncryptedCards(true); }) } const showHiddenCardsCheckbox = document.getElementById('admin-show-hidden-checkbox') if (showHiddenCardsCheckbox) { showHiddenCardsCheckbox.addEventListener('change', async (event) => { await fetchAllEncryptedCards(true) }) } document.getElementById("publish-card-form").addEventListener("submit", async (event) => { event.preventDefault() const isTopicChecked = document.getElementById("topic-checkbox").checked // Pass that boolean to publishEncryptedCard await publishEncryptedCard(isTopicChecked) }) document.getElementById("sort-select").addEventListener("change", async () => { // Re-load the cards whenever user chooses a new sort option. await fetchAllEncryptedCards() }) createScrollToTopButton() // await fetchAndValidateAllAdminCards() await updateOrSaveAdminGroupsDataLocally() await fetchAllKicKBanTxData() await fetchAllEncryptedCards() } const fetchAllKicKBanTxData = async () => { const kickTxType = "GROUP_KICK" const banTxType = "GROUP_BAN" // Helper function to filter transactions const filterTransactions = (rawTransactions) => { // Group transactions by member const memberTxMap = rawTransactions.reduce((map, tx) => { if (!map[tx.member]) { map[tx.member] = [] } map[tx.member].push(tx) return map }, {}) // Filter out members with both pending and non-pending transactions return Object.values(memberTxMap) .filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING')) .flat() // .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') && // txs.some(tx => tx.approvalStatus !== 'PENDING'))) // .flat() } // Fetch ban transactions const rawBanTransactions = await searchTransactions({ txTypes: [banTxType], address: '', confirmationStatus: 'CONFIRMED', limit: 0, reverse: true, offset: 0, startBlock: 1990000, blockLimit: 0, txGroupId: 0, }) // Filter transactions for bans banTransactions = filterTransactions(rawBanTransactions) console.warn('banTxData (filtered):', banTransactions) // Fetch kick transactions const rawKickTransactions = await searchTransactions({ txTypes: [kickTxType], address: '', confirmationStatus: 'CONFIRMED', limit: 0, reverse: true, offset: 0, startBlock: 1990000, blockLimit: 0, txGroupId: 0, }) // Filter transactions for kicks kickTransactions = filterTransactions(rawKickTransactions) console.warn('kickTxData (filtered):', kickTransactions) } // Example: fetch and save admin public keys and count const updateOrSaveAdminGroupsDataLocally = async () => { try { // Fetch the array of admin public keys const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys() // Build an object containing the count and the array const adminData = { keysCount: verifiedAdminPublicKeys.length, publicKeys: verifiedAdminPublicKeys } adminPublicKeys = verifiedAdminPublicKeys // Stringify and save to localStorage localStorage.setItem('savedAdminData', JSON.stringify(adminData)) console.log('Admin public keys saved locally:', adminData) } catch (error) { console.error('Error fetching/storing admin public keys:', error) attemptLoadAdminDataCount++ } } const loadOrFetchAdminGroupsData = async () => { try { // Pull the JSON from localStorage const storedData = localStorage.getItem('savedAdminData') if (!storedData && attemptLoadAdminDataCount <= 3) { console.log('No saved admin public keys found in local storage. Fetching...') await updateOrSaveAdminGroupsDataLocally() attemptLoadAdminDataCount++ return null } // Parse the JSON, then store the global variables. const parsedData = JSON.parse(storedData) adminMemberCount = parsedData.keysCount adminPublicKeys = parsedData.publicKeys console.log(typeof adminPublicKeys); // Should be "object" console.log(Array.isArray(adminPublicKeys)) console.log(`Loaded admins 'keysCount'=${adminMemberCount}, publicKeys=`, adminPublicKeys) attemptLoadAdminDataCount = 0 return parsedData // and return { adminMemberCount, adminKeys } to the caller } catch (error) { console.error('Error loading/parsing saved admin public keys:', error) return null } } const extractEncryptedCardsMinterName = (cardIdentifier) => { const parts = cardIdentifier.split('-') // Ensure the format has at least 3 parts if (parts.length < 3) { throw new Error('Invalid identifier format') } if (parts.slice(2, -1).join('-') === 'TOPIC') { console.log(`TOPIC found in identifier: ${cardIdentifier} - not including in duplicatesList`) return } // 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 fetchAllEncryptedCards = async (isRefresh = false) => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container") encryptedCardsContainer.innerHTML = "

Loading cards...

" try { const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0) if (!response || !Array.isArray(response) || response.length === 0) { encryptedCardsContainer.innerHTML = "

No cards found.

" return } // Validate and decrypt cards asynchronously const validatedCards = await Promise.all( response.map(async (card) => { try { // Validate the card identifier const isValid = await validateEncryptedCardIdentifier(card) if (!isValid) return null // Fetch and decrypt the card data const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: card.name, service: "MAIL_PRIVATE", identifier: card.identifier, encoding: "base64", }) if (!cardDataResponse) return null const decryptedCardData = await decryptAndParseObject(cardDataResponse) // Skip cards without polls if (!decryptedCardData.poll) return null return { card, decryptedCardData } } catch (error) { console.warn(`Error processing card ${card.identifier}:`, error) return null } }) ) // Filter out invalid or skipped cards const validCardsWithData = validatedCards.filter((entry) => entry !== null) if (validCardsWithData.length === 0) { encryptedCardsContainer.innerHTML = "

No valid cards found.

" return } // Combine `processCards` logic: Deduplicate cards by identifier and keep latest timestamp const latestCardsMap = new Map() validCardsWithData.forEach(({ card, decryptedCardData }) => { const timestamp = card.updated || card.created || 0 const existingCard = latestCardsMap.get(card.identifier) if (!existingCard || timestamp > (existingCard.card.updated || existingCard.card.created || 0)) { latestCardsMap.set(card.identifier, { card, decryptedCardData }) } }) const uniqueValidCards = Array.from(latestCardsMap.values()) // Map to track the most recent card per minterName const mostRecentCardsMap = new Map() uniqueValidCards.forEach(({ card, decryptedCardData }) => { const obtainedMinterName = decryptedCardData.minterName // Only check for cards that are NOT topic-based cards if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') { const cardTimestamp = card.updated || card.created || 0 if (obtainedMinterName) { const existingEntry = mostRecentCardsMap.get(obtainedMinterName) // Replace only if the current card is more recent if (!existingEntry || cardTimestamp > (existingEntry.card.updated || existingEntry.card.created || 0)) { mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData }) } } } else { console.log(`topic card detected, skipping most recent by name mapping...`) // We still need to add the topic-based cards to the map, as it will be utilized in the next step mostRecentCardsMap.set(obtainedMinterName, {card, decryptedCardData}) } }) // Convert the map into an array of final cards const finalCards = Array.from(mostRecentCardsMap.values()); let selectedSort = 'newest' const sortSelect = document.getElementById('sort-select') if (sortSelect) { selectedSort = sortSelect.value } if (selectedSort === 'name') { // Sort alphabetically by the minter's name finalCards.sort((a, b) => { const nameA = a.decryptedCardData.minterName?.toLowerCase() || '' const nameB = b.decryptedCardData.minterName?.toLowerCase() || '' return nameA.localeCompare(nameB) }) } else if (selectedSort === 'recent-comments') { // We need each card's newest comment timestamp for sorting for (let card of finalCards) { card.newestCommentTimestamp = await getNewestAdminCommentTimestamp(card.card.identifier) } // Then sort descending by newest comment finalCards.sort((a, b) => (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0) ) } else if (selectedSort === 'least-votes') { // TODO: Add the logic to sort by LEAST total ADMIN votes, then totalYesWeight const minterGroupMembers = await fetchMinterGroupMembers() const minterAdmins = await fetchMinterGroupAdmins() for (const finalCard of finalCards) { try { const pollName = finalCard.decryptedCardData.poll // If card or poll is missing, default to zero if (!pollName) { finalCard._adminTotalVotes = 0 finalCard._yesWeight = 0 continue } const pollResults = await fetchPollResults(pollName) if (!pollResults || pollResults.error) { finalCard._adminTotalVotes = 0 finalCard._yesWeight = 0 continue } // Pull only the adminYes/adminNo/totalYesWeight from processPollData const { adminYes, adminNo, totalYesWeight } = await processPollData( pollResults, minterGroupMembers, minterAdmins, finalCard.decryptedCardData.creator, finalCard.card.identifier ) finalCard._adminTotalVotes = adminYes + adminNo finalCard._yesWeight = totalYesWeight } catch (error) { console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error) finalCard._adminTotalVotes = 0 finalCard._yesWeight = 0 } } // Sort ascending by (adminYes + adminNo), then descending by totalYesWeight finalCards.sort((a, b) => { const diffAdminTotal = a._adminTotalVotes - b._adminTotalVotes if (diffAdminTotal !== 0) return diffAdminTotal // If there's a tie, show the card with higher yesWeight first return b._yesWeight - a._yesWeight }) } else if (selectedSort === 'most-votes') { // TODO: Add the logic to sort by MOST total ADMIN votes, then totalYesWeight const minterGroupMembers = await fetchMinterGroupMembers() const minterAdmins = await fetchMinterGroupAdmins() for (const finalCard of finalCards) { try { const pollName = finalCard.decryptedCardData.poll if (!pollName) { finalCard._adminTotalVotes = 0 finalCard._yesWeight = 0 continue } const pollResults = await fetchPollResults(pollName) if (!pollResults || pollResults.error) { finalCard._adminTotalVotes = 0 finalCard._yesWeight = 0 continue } const { adminYes, adminNo, totalYesWeight } = await processPollData( pollResults, minterGroupMembers, minterAdmins, finalCard.decryptedCardData.creator, finalCard.card.identifier ) finalCard._adminTotalVotes = adminYes + adminNo finalCard._yesWeight = totalYesWeight } catch (error) { console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error) finalCard._adminTotalVotes = 0 finalCard._yesWeight = 0 } } // Sort descending by (adminYes + adminNo), then descending by totalYesWeight finalCards.sort((a, b) => { const diffAdminTotal = b._adminTotalVotes - a._adminTotalVotes if (diffAdminTotal !== 0) return diffAdminTotal return b._yesWeight - a._yesWeight }) } else { // Sort cards by timestamp (most recent first) finalCards.sort((a, b) => { const timestampA = a.card.updated || a.card.created || 0 const timestampB = b.card.updated || b.card.created || 0 return timestampB - timestampA; }) } encryptedCardsContainer.innerHTML = "" const finalVisualFilterCards = finalCards.filter(({card}) => { const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false if (!showKickedBanned){ if (adminBoardState.bannedCards.has(card.identifier)) { return false // skip } if (adminBoardState.kickedCards.has(card.identifier)) { return false // skip } } if (!showHiddenAdminCards) { if (adminBoardState.hiddenList.has(card.identifier)) { return false // skip } } return true }) console.warn(`sharing current adminBoardState...`,adminBoardState) // Display skeleton cards immediately finalVisualFilterCards.forEach(({ card }) => { const skeletonHTML = createSkeletonCardHTML(card.identifier) encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) }) // Fetch poll results and update each card await Promise.all( finalVisualFilterCards.map(async ({ card, decryptedCardData }) => { try { // Validate poll publisher keys const encryptedCardPollPublisherPublicKey = await getPollPublisherPublicKey(decryptedCardData.poll) const encryptedCardPublisherPublicKey = await getPublicKeyByName(card.name) if (encryptedCardPollPublisherPublicKey !== encryptedCardPublisherPublicKey) { console.warn(`QuickMythril cardPollHijack attack detected! Skipping card: ${card.identifier}`) removeSkeleton(card.identifier) return } // Fetch poll results const pollResults = await fetchPollResults(decryptedCardData.poll) if (pollResults?.error) { console.warn(`Skipping card with failed poll results: ${card.identifier}`) removeSkeleton(card.identifier); return; } const encryptedCommentCount = await getEncryptedCommentCount(card.identifier) // Generate final card HTML const finalCardHTML = await createEncryptedCardHTML( decryptedCardData, pollResults, card.identifier, encryptedCommentCount ) if ((!finalCardHTML) || (finalCardHTML === '')){ removeSkeleton(card.identifier) } replaceSkeleton(card.identifier, finalCardHTML) } catch (error) { console.error(`Error finalizing card ${card.identifier}:`, error) removeSkeleton(card.identifier) } }) ) } catch (error) { console.error("Error loading cards:", error) encryptedCardsContainer.innerHTML = "

Failed to load cards.

" } } // Function to create a skeleton card const createEncryptedSkeletonCardHTML = (cardIdentifier) => { return `
` } // Function to check and fech an existing Minter Card if attempting to publish twice ---------------------------------------- const fetchExistingEncryptedCard = async (minterName, existingIdentifier) => { try{ const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: minterName, service: "MAIL_PRIVATE", identifier: existingIdentifier, encoding: "base64" }) const decryptedCardData = await decryptAndParseObject(cardDataResponse) console.log("Full card data fetched successfully:", decryptedCardData) return decryptedCardData } 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.identifier.includes("card-MAC-NC-function now() { [native code] }-Y6CmuY") && // Added check for failed name card publish due to identifier issue. card.created ) } // Load existing card data passed, into the form for editing ------------------------------------- const loadEncryptedCardIntoForm = async (decryptedCardData) => { if (decryptedCardData) { console.log("Loading existing card data:", decryptedCardData) document.getElementById("minter-name-input").value = decryptedCardData.minterName document.getElementById("card-header").value = decryptedCardData.header document.getElementById("card-content").value = decryptedCardData.content const linksContainer = document.getElementById("links-container") linksContainer.innerHTML = ""; // Clear previous links decryptedCardData.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) const name = nameInfo.name if (name) { console.log(`name information found, returning:`, name) return name } else { console.warn(`no name information found, this is not a registered name: '${minterName}', Returning null`, name) return null } } catch (error){ console.error(`extracting name from name info: ${minterName} failed.`, error) return null } } const publishEncryptedCard = async (isTopicModePassed = false) => { // If the user wants it to be a topic, we set global isTopic = true, else false isTopic = isTopicModePassed || isTopic const minterNameInput = document.getElementById("minter-name-input").value.trim() const header = document.getElementById("card-header").value.trim() const content = document.getElementById("card-content").value.trim() const links = Array.from(document.querySelectorAll(".card-link")) .map(input => input.value.trim()) .filter(link => link.startsWith("qortal://")) // Basic validation if (!header || !content) { alert("Header and Content are required!") return } let publishedMinterName = minterNameInput // If not topic mode, validate the user actually entered a valid Minter name if (!isTopic) { publishedMinterName = await validateMinterName(minterNameInput) if (!publishedMinterName) { alert(`"${minterNameInput}" doesn't seem to be a valid name. Please check or use topic mode.`) return } // Also check for existing card if not topic if (!isUpdateCard && existingCardMinterNames.some(item => item.minterName === publishedMinterName)) { const duplicateCardData = existingCardMinterNames.find(item => item.minterName === publishedMinterName) const updateCard = confirm( `Minter Name: ${publishedMinterName} already has a card. Duplicate name-based cards are not allowed. You can OVERWRITE it or Cancel publishing. UPDATE CARD?` ) if (updateCard) { existingEncryptedCardIdentifier = duplicateCardData.identifier isUpdateCard = true } else { return } } } // Determine final card identifier const currentTimestamp = Date.now() const newCardIdentifier = isTopic ? `${encryptedCardIdentifierPrefix}-TOPIC-${await uid()}` : `${encryptedCardIdentifierPrefix}-NC-${currentTimestamp}-${await uid()}` const cardIdentifier = isUpdateCard ? existingEncryptedCardIdentifier : newCardIdentifier // Build cardData const pollName = `${cardIdentifier}-poll` const cardData = { minterName: publishedMinterName, header, content, links, creator: userState.accountName, timestamp: Date.now(), poll: pollName, topicMode: isTopic } try { // Convert to base64 or fallback let base64CardData = await objectToBase64(cardData) if (!base64CardData) { base64CardData = btoa(JSON.stringify(cardData)) } let verifiedAdminPublicKeys = adminPublicKeys if ((!verifiedAdminPublicKeys) || verifiedAdminPublicKeys.length <= 5 || !Array.isArray(verifiedAdminPublicKeys)) { console.log(`adminPublicKeys variable failed check, attempting to load from localStorage`,adminPublicKeys) const savedAdminData = localStorage.getItem('savedAdminData') const parsedAdminData = JSON.parse(savedAdminData) const loadedAdminKeys = parsedAdminData.publicKeys if ((!loadedAdminKeys) || (!Array.isArray(loadedAdminKeys)) || (loadedAdminKeys.length === 0)){ console.log('loaded admin keys from localStorage failed, falling back to API call...') verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys() } verifiedAdminPublicKeys = loadedAdminKeys } await qortalRequest({ action: "PUBLISH_QDN_RESOURCE", name: userState.accountName, service: "MAIL_PRIVATE", identifier: cardIdentifier, data64: base64CardData, encrypt: true, publicKeys: verifiedAdminPublicKeys }) // Possibly create a poll if it's a brand new card if (!isUpdateCard) { await qortalRequest({ action: "CREATE_POLL", pollName, pollDescription: `Admin Board Poll Published By ${userState.accountName}`, pollOptions: ["Yes, No"], pollOwnerAddress: userState.accountAddress }) alert("Card and poll published successfully!") } else { alert("Card updated successfully! (No poll updates possible currently...)"); } document.getElementById("publish-card-form").reset() document.getElementById("publish-card-view").style.display = "none" document.getElementById("encrypted-cards-container").style.display = "flex" isTopic = false; // reset global } 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 searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0) 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() const commentData = { content: commentText, creator: userState.accountName, timestamp: postTimestamp, } const commentIdentifier = `comment-${cardIdentifier}-${await uid()}` if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0) || (!adminPublicKeys)) { console.log('adminPpublicKeys variable failed checks, calling for admin public keys from API (comment)',adminPublicKeys) 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 = '' } 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 searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0, 0, '', false) if (response) { return response } } catch (error) { console.error(`Error fetching comments for ${cardIdentifier}:`, error) return [] } } const displayEncryptedComments = async (cardIdentifier) => { try { const comments = await fetchEncryptedComments(cardIdentifier) const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) commentsContainer.innerHTML = '' const voterMap = globalVoterMap.get(cardIdentifier) || new Map() const commentHTMLArray = await Promise.all( comments.map(async (comment) => { try { 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) const commenter = decryptedCommentData.creator const voterInfo = voterMap.get(commenter) let commentColor = "transparent" let adminBadge = "" if (voterInfo) { if (voterInfo.voterType === "Admin") { // Admin-specific colors commentColor = voterInfo.vote === "yes" ? "rgba(25, 175, 25, 0.6)" : "rgba(194, 39, 62, 0.6)" // Light green for yes, light red for no const badgeColor = voterInfo.vote === "yes" ? "green" : "red" adminBadge = `(Admin)` } else { // Non-admin colors commentColor = voterInfo.vote === "yes" ? "rgba(0, 100, 0, 0.3)" : "rgba(100, 0, 0, 0.3)" // Darker green for yes, darker red for no } } return `

${decryptedCommentData.creator} ${adminBadge}

${decryptedCommentData.content}

${timestamp}

` } catch (err) { console.error(`Error processing comment ${comment.identifier}:`, err) return null // Skip this comment if it fails } }) ) // Add all comments to the container commentHTMLArray .filter((html) => html !== null) // Filter out failed comments .forEach((commentHTML) => { commentsContainer.insertAdjacentHTML('beforeend', commentHTML) }) } catch (error) { console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) } } const toggleEncryptedComments = async (cardIdentifier) => { const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`) const commentButton = document.getElementById(`comment-button-${cardIdentifier}`) if (!commentsSection || !commentButton) return const count = commentButton.dataset.commentCount; const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display) if (isHidden) { // Show comments commentButton.textContent = "LOADING..." await displayEncryptedComments(cardIdentifier) commentsSection.style.display = 'block' // Change the button text to 'HIDE COMMENTS' commentButton.textContent = 'HIDE COMMENTS' } else { // Hide comments commentsSection.style.display = 'none' commentButton.textContent = `COMMENTS (${count})` } } const createLinkDisplayModal = async () => { const modalHTML = ` ` 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('links-modal') const modalContent = document.getElementById('links-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('links-modal') const modalContent = document.getElementById('links-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() 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)) return `/render/${firstParam}${remainingPath}?theme=${themeColor}` } } return link } const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => { const latestBlockInfo = await getLatestBlockInfo() const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT let minAdminCount const minterAdmins = await fetchMinterGroupAdmins() if ((minterAdmins) && (minterAdmins.length === 1)){ console.warn(`simply a double-check that there is only one MINTER group admin, in which case the group hasn't been transferred to null...keeping default minAdminCount of: ${minAdminCount}`) minAdminCount = 9 } else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){ const totalAdmins = minterAdmins.length const fortyPercent = totalAdmins * 0.40 minAdminCount = Math.round(fortyPercent) console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`) } if (isBlockPassed && userState.isMinterAdmin) { console.warn(`feature trigger has passed, checking for approval requirements`) const addressInfo = await getNameInfo(name) const address = addressInfo.owner const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK") const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN") if (kickApprovalHtml) { return kickApprovalHtml } if (banApprovalHtml) { return banApprovalHtml } } if (adminYes >= minAdminCount && userState.isMinterAdmin) { const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier) return removeButtonHtml } else{ return '' } } const createRemoveButtonHtml = (name, cardIdentifier) => { return `
` } const handleKickMinter = async (minterName) => { try { // Optional block check let txGroupId = 0 // const { height: currentHeight } = await getLatestBlockInfo() const isBlockPassed = await featureTriggerCheck() if (isBlockPassed) { console.log(`block height above featureTrigger Height, using group approval method...txGroupId 694`) txGroupId = 694 } // Get the minter address from name info const minterNameInfo = await getNameInfo(minterName) const minterAddress = minterNameInfo?.owner if (!minterAddress) { alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`) return } const adminPublicKey = await getPublicKeyByName(userState.accountName) const reason = 'Kicked by Minter Admins' const fee = 0.01 const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee) const signedKickTransaction = await qortalRequest({ action: "SIGN_TRANSACTION", unsignedBytes: rawKickTransaction }) if (!signedKickTransaction) { console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`) alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`) return } let txToProcess = signedKickTransaction const processKickTx = await processTransaction(txToProcess) if (typeof processKickTx === 'object') { console.log("transaction success object:", processKickTx) alert(`${minterName} kick successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processKickTx)}`) } else { console.log("transaction raw text response:", processKickTx) alert(`TxResponse: ${JSON.stringify(processKickTx)}`) } } catch (error) { console.error("Error removing minter:", error) alert(`Error:${error}. Please try again.`) } } const handleBanMinter = async (minterName) => { try { let txGroupId = 0 // const { height: currentHeight } = await getLatestBlockInfo() const isBlockPassed = await featureTriggerCheck() if (!isBlockPassed) { console.log(`block height is under the removal featureTrigger height, using txGroupId 0`) txGroupId = 0 } else { console.log(`featureTrigger block is passed, using txGroupId 694`) txGroupId = 694 } const minterNameInfo = await getNameInfo(minterName) const minterAddress = minterNameInfo?.owner if (!minterAddress) { alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`) return } const adminPublicKey = await getPublicKeyByName(userState.accountName) const reason = 'Banned by Minter Admins' const fee = 0.01 const rawBanTransaction = await createGroupBanTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee) const signedBanTransaction = await qortalRequest({ action: "SIGN_TRANSACTION", unsignedBytes: rawBanTransaction }) if (!signedBanTransaction) { console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`) alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`) return } let txToProcess = signedBanTransaction const processedTx = await processTransaction(txToProcess) if (typeof processedTx === 'object') { console.log("transaction success object:", processedTx) alert(`${minterName} BAN successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processedTx)}`) } else { // fallback string or something console.log("transaction raw text response:", processedTx) alert(`transaction response:${JSON.stringify(processedTx)}` ) } } catch (error) { console.error("Error removing minter:", error) alert(`Error ${error}. Please try again.`) } } const getNewestAdminCommentTimestamp = async (cardIdentifier) => { try { const comments = await fetchEncryptedComments(cardIdentifier) if (!comments || comments.length === 0) { return 0 } const newestTimestamp = comments.reduce((acc, comment) => { const cTime = comment.updated || comment.created || 0 return cTime > acc ? cTime : acc }, 0) return newestTimestamp } catch (err) { console.error('Failed to get newest comment timestamp:', err) return 0 } } // Create the overall Minter Card HTML ----------------------------------------------- const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData const formattedDate = new Date(timestamp).toLocaleString() const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null const creatorAvatar = await getMinterAvatar(creator) const linksHTML = links.map((link, index) => ` `).join("") const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false const isUndefinedUser = (minterName === 'undefined') const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode') let showTopic = false if (hasTopicMode) { const modeVal = cardData.topicMode showTopic = (modeVal === true || modeVal === 'true') } else { if (!isUndefinedUser) { showTopic = false } } let cardColorCode = showTopic ? '#0e1b15' : '#151f28' const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
REGARDING (Topic):

${minterName}` : `
REGARDING (Name):
${minterAvatar}

${minterName}` 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, detailsHtml, userVote = null } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier) createModal('links') createModal('poll-details') let showRemoveHtml let altText = '' let penaltyText = '' let adjustmentText = '' const verifiedName = await validateMinterName(minterName) let levelText = '

' if (verifiedName) { const accountInfo = await getNameInfo(verifiedName) const accountAddress = accountInfo.owner const addressInfo = await getAddressInfo(accountAddress) levelText = ` - Level ${addressInfo.level}` console.log(`name is validated, utilizing for removal features...${verifiedName}`) penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '

(has Blocks Penalty)

' adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '

(has Blocks Adjustment)

' const removeActionsHtml = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier) showRemoveHtml = removeActionsHtml if (userVote === 0) { cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want } else if (userVote === 1) { cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want } if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){ console.warn(`account was already banned, displaying as such...`) cardColorCode = 'rgb(24, 3, 3)' altText = `

BANNED From MINTER Group

` showRemoveHtml = '' if (!adminBoardState.bannedCards.has(cardIdentifier)){ adminBoardState.bannedCards.add(cardIdentifier) } if (!showKickedBanned){ console.warn(`kick/bank checkbox is unchecked, and card is banned, not displaying...`) return '' } } if (kickTransactions.some((kickTx) => kickTx.groupId === 694 && kickTx.member === accountAddress)){ console.warn(`account was already kicked, displaying as such...`) cardColorCode = 'rgb(29, 7, 4)' altText = `

KICKED From MINTER Group

` showRemoveHtml = '' if (!adminBoardState.kickedCards.has(cardIdentifier)){ adminBoardState.kickedCards.add(cardIdentifier) } if (!showKickedBanned) { console.warn(`kick/ban checkbox is unchecked, card is kicked, not displaying...`) return '' } } } else { console.log(`name could not be validated, assuming topic card (or some other issue with name validation) for removalActions`) showRemoveHtml = '' } return `

Created By:

${creatorAvatar}

${creator}

${minterOrTopicHtml}${levelText}

${header}

${penaltyText}${adjustmentText}${altText}
${content}
LINKS
CURRENT RESULTS
${showRemoveHtml}
Admin Support: ${adminYes} Admin Against: ${adminNo}
Supporting Weight ${totalYesWeight} Denial Weight ${totalNoWeight}
ACTIONS FOR
${minterName}

(click COMMENTS button to open/close card comments)

By: ${creator} - ${formattedDate}

` }