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'.
"
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)) ? `
'
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 `
${decryptedCommentData.creator} ${adminBadge}
${decryptedCommentData.content}
${timestamp}