2024-12-19 03:39:15 +00:00
// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
const isEncryptedTestMode = false
2024-12-24 08:27:17 +00:00
const encryptedCardIdentifierPrefix = "card-MAC"
2025-01-01 03:48:34 +00:00
let isUpdateCard = false
2024-12-20 05:28:36 +00:00
let existingDecryptedCardData = { }
let existingEncryptedCardIdentifier = { }
2024-12-19 03:39:15 +00:00
let cardMinterName = { }
let existingCardMinterNames = [ ]
2024-12-24 08:27:17 +00:00
let isTopic = false
let attemptLoadAdminDataCount = 0
let adminMemberCount = 0
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
let adminPublicKeys = [ ]
2024-12-19 03:39:15 +00:00
2024-12-31 05:39:18 +00:00
console . log ( "Attempting to load AdminBoard.js" )
2024-12-19 03:39:15 +00:00
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" ) ) {
2025-01-01 03:48:34 +00:00
child . remove ( )
2024-12-19 03:39:15 +00:00
}
}
// Add the "Minter Board" content
2024-12-31 05:39:18 +00:00
const mainContent = document . createElement ( "div" )
2024-12-19 03:39:15 +00:00
mainContent . innerHTML = `
< div class = "minter-board-main" style = "padding: 20px; text-align: center;" >
< h1 style = "color: lightblue;" > AdminBoard < / h 1 >
< 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 < / b u t t o n >
< button id = "refresh-cards-button" class = "refresh-cards-button" style = "padding: 10px;" > Refresh Cards < / b u t t o n >
< div id = "encrypted-cards-container" class = "cards-container" style = "margin-top: 20px;" > < / d i v >
< div id = "publish-card-view" class = "publish-card-view" style = "display: none; text-align: left; padding: 20px;" >
< form id = "publish-card-form" >
2025-01-01 03:48:34 +00:00
< h3 > Create or Update an Admin Card < / h 3 >
2024-12-24 08:27:17 +00:00
< div class = "publish-card-checkbox" style = "margin-top: 1em;" >
< input type = "checkbox" id = "topic-checkbox" name = "topicMode" / >
< label for = "topic-checkbox" > Is this a Topic instead of a Minter ? < / l a b e l >
< / d i v >
2025-01-01 03:48:34 +00:00
< label for = "minter-name-input" > Input TOPIC or NAME : < / l a b e l >
< input type = "text" id = "minter-name-input" maxlength = "100" placeholder = "input NAME or TOPIC" required >
2024-12-19 03:39:15 +00:00
< label for = "card-header" > Header : < / l a b e l >
< input type = "text" id = "card-header" maxlength = "100" placeholder = "Explain main point/issue" required >
< label for = "card-content" > Content : < / l a b e l >
2025-01-01 03:48:34 +00:00
< textarea id = "card-content" placeholder = "Enter any information you like... CHECK THE TOPIC CHECKBOX if you do not want to publish a NAME card. NAME cards are verified and can only be one per name. Links are displayed in in-app pop-up." required > < / t e x t a r e a >
2024-12-19 03:39:15 +00:00
< label for = "card-links" > Links ( qortal : //...):</label>
< div id = "links-container" >
< input type = "text" class = "card-link" placeholder = "Enter QDN link" >
< / d i v >
< button type = "button" id = "add-link-button" > Add Another Link < / b u t t o n >
< button type = "submit" id = "submit-publish-button" > Publish Card < / b u t t o n >
< button type = "button" id = "cancel-publish-button" > Cancel < / b u t t o n >
< / f o r m >
< / d i v >
< / d i v >
2024-12-31 05:39:18 +00:00
`
document . body . appendChild ( mainContent )
2024-12-19 03:39:15 +00:00
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>"
2025-01-01 03:48:34 +00:00
await fetchAllEncryptedCards ( true )
2024-12-19 03:39:15 +00:00
} )
}
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 ) => {
2024-12-31 05:39:18 +00:00
event . preventDefault ( )
const isTopicChecked = document . getElementById ( "topic-checkbox" ) . checked
2024-12-24 08:27:17 +00:00
// Pass that boolean to publishEncryptedCard
2024-12-31 05:39:18 +00:00
await publishEncryptedCard ( isTopicChecked )
} )
2025-01-05 04:28:26 +00:00
createScrollToTopButton ( )
2024-12-31 05:39:18 +00:00
// await fetchAndValidateAllAdminCards()
await fetchAllEncryptedCards ( )
await updateOrSaveAdminGroupsDataLocally ( )
2024-12-24 08:27:17 +00:00
}
// Example: fetch and save admin public keys and count
const updateOrSaveAdminGroupsDataLocally = async ( ) => {
try {
// Fetch the array of admin public keys
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys ( )
2024-12-24 08:27:17 +00:00
// Build an object containing the count and the array
const adminData = {
keysCount : verifiedAdminPublicKeys . length ,
publicKeys : verifiedAdminPublicKeys
} ;
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
adminPublicKeys = verifiedAdminPublicKeys
2024-12-24 08:27:17 +00:00
// Stringify and save to localStorage
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
localStorage . setItem ( 'savedAdminData' , JSON . stringify ( adminData ) )
2024-12-24 08:27:17 +00:00
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
console . log ( 'Admin public keys saved locally:' , adminData )
2024-12-24 08:27:17 +00:00
} catch ( error ) {
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
console . error ( 'Error fetching/storing admin public keys:' , error )
2024-12-24 08:27:17 +00:00
attemptLoadAdminDataCount ++
}
2024-12-31 05:39:18 +00:00
}
2024-12-24 08:27:17 +00:00
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
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
console . log ( typeof adminPublicKeys ) ; // Should be "object"
console . log ( Array . isArray ( adminPublicKeys ) )
console . log ( ` Loaded admins 'keysCount'= ${ adminMemberCount } , publicKeys= ` , adminPublicKeys )
2024-12-24 08:27:17 +00:00
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
}
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
const extractEncryptedCardsMinterName = ( cardIdentifier ) => {
2024-12-19 03:39:15 +00:00
const parts = cardIdentifier . split ( '-' ) ;
// Ensure the format has at least 3 parts
if ( parts . length < 3 ) {
throw new Error ( 'Invalid identifier format' ) ;
}
2024-12-28 07:04:16 +00:00
2024-12-24 08:27:17 +00:00
if ( parts . slice ( 2 , - 1 ) . join ( '-' ) === 'TOPIC' ) {
console . log ( ` TOPIC found in identifier: ${ cardIdentifier } - not including in duplicatesList ` )
2024-12-29 06:49:18 +00:00
return
2024-12-24 08:27:17 +00:00
}
2024-12-19 03:39:15 +00:00
// Extract minterName (everything from the second part to the second-to-last part)
2024-12-31 05:39:18 +00:00
const minterName = parts . slice ( 2 , - 1 ) . join ( '-' )
2024-12-19 03:39:15 +00:00
// Return the extracted minterName
2024-12-31 05:39:18 +00:00
return minterName
2024-12-19 03:39:15 +00:00
}
2025-01-02 21:27:31 +00:00
// const processCards = async (validEncryptedCards) => {
// const latestCardsMap = new Map()
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// await Promise.all(validEncryptedCards.map(async card => {
// const timestamp = card.updated || card.created || 0
// const existingCard = latestCardsMap.get(card.identifier)
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
// latestCardsMap.set(card.identifier, card)
// }
// }))
2024-12-28 07:04:16 +00:00
2025-01-02 21:27:31 +00:00
// console.log(`latestCardsMap, by timestamp`, latestCardsMap)
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// const uniqueValidCards = Array.from(latestCardsMap.values())
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// return uniqueValidCards
// }
2024-12-20 05:28:36 +00:00
2024-12-19 03:39:15 +00:00
//Main function to load the Minter Cards ----------------------------------------
2025-01-02 21:27:31 +00:00
//TODO verify the latest changes work
// const fetchAllEncryptedCards = async (isRefresh=false) => {
// const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
// encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
// try {
// const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0)
// 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
// })
// )
// console.log(`validatedEncryptedCards:`, validatedEncryptedCards, `... running next filter...`)
// const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null)
// console.log(`validEncryptedcards:`, validEncryptedCards)
// if (validEncryptedCards.length === 0) {
// encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>";
// return;
// }
// const finalCards = await processCards(validEncryptedCards)
// console.log(`finalCards:`,finalCards)
// // 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 hasMinterName = await extractEncryptedCardsMinterName(card.identifier)
// // if (hasMinterName) existingCardMinterNames.push(hasMinterName)
// const cardDataResponse = await qortalRequest({
// action: "FETCH_QDN_RESOURCE",
// name: card.name,
// service: "MAIL_PRIVATE",
// identifier: card.identifier,
// encoding: "base64"
// })
// if (!cardDataResponse) {
// console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
// removeSkeleton(card.identifier)
// return
// }
// 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
// }
// const encryptedCardPollPublisherPublicKey = await getPollPublisherPublicKey(decryptedCardData.poll)
// const encryptedCardPublisherPublicKey = await getPublicKeyByName(card.name)
// if (encryptedCardPollPublisherPublicKey != encryptedCardPublisherPublicKey) {
// console.warn(`QuickMythril cardPollHijack attack found! Not including card with identifier: ${card.identifier}`)
// removeSkeleton(card.identifier)
// return
// }
// // Fetch poll results and discard cards with no results
// const pollResults = await fetchPollResults(decryptedCardData.poll)
// if (pollResults?.error) {
// console.warn(`Skipping card with failed poll results?: ${card.identifier}, poll=${decryptedCardData.poll}`)
// removeSkeleton(card.identifier)
// return
// }
// if (!isRefresh) {
// console.log(`This is a REFRESH, NOT adding names to duplicates list...`)
// const obtainedMinterName = decryptedCardData.minterName
// // if ((obtainedMinterName) && existingCardMinterNames.includes(obtainedMinterName)) {
// // console.warn(`name found in existing names array...${obtainedMinterName} skipping duplicate card...${card.identifier}`)
// // removeSkeleton(card.identifier)
// // return
// // } else if ((obtainedMinterName) && (!existingCardMinterNames.includes(obtainedMinterName))) {
// // existingCardMinterNames.push(obtainedMinterName)
// // console.log(`minterName: ${obtainedMinterName} found, doesn't exist in existing array, added to existingCardMinterNames array`)
// // }
// if (obtainedMinterName && existingCardMinterNames.some(item => item.minterName === obtainedMinterName)) {
// console.warn(`name found in existing names array...${obtainedMinterName} skipping duplicate card...${card.identifier}`)
// removeSkeleton(card.identifier)
// return
// } else if (obtainedMinterName) {
// existingCardMinterNames.push({ minterName: obtainedMinterName, identifier: card.identifier })
// console.log(`Added minterName and identifier to existingCardMinterNames array:`, { minterName: obtainedMinterName, identifier: card.identifier })
// }
// }
// // 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)
// replaceSkeleton(card.identifier, finalCardHTML)
// } catch (error) {
// console.error(`Error processing card ${card.identifier}:`, error)
// removeSkeleton(card.identifier)
// }
// })
// } catch (error) {
// console.error("Error loading cards:", error)
// encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>"
// }
// }
const fetchAllEncryptedCards = async ( isRefresh = false ) => {
2024-12-31 05:39:18 +00:00
const encryptedCardsContainer = document . getElementById ( "encrypted-cards-container" )
encryptedCardsContainer . innerHTML = "<p>Loading cards...</p>"
2024-12-19 03:39:15 +00:00
try {
2024-12-28 07:04:16 +00:00
const response = await searchSimple ( 'MAIL_PRIVATE' , ` ${ encryptedCardIdentifierPrefix } ` , '' , 0 )
2024-12-19 03:39:15 +00:00
if ( ! response || ! Array . isArray ( response ) || response . length === 0 ) {
2024-12-31 05:39:18 +00:00
encryptedCardsContainer . innerHTML = "<p>No cards found.</p>"
return
2024-12-19 03:39:15 +00:00
}
2025-01-02 21:27:31 +00:00
// 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
}
2024-12-19 03:39:15 +00:00
} )
2024-12-28 07:04:16 +00:00
)
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// Filter out invalid or skipped cards
const validCardsWithData = validatedCards . filter ( ( entry ) => entry !== null )
if ( validCardsWithData . length === 0 ) {
encryptedCardsContainer . innerHTML = "<p>No valid cards found.</p>"
2024-12-19 03:39:15 +00:00
return ;
}
2025-01-02 21:27:31 +00:00
// 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 } )
}
2024-12-31 05:39:18 +00:00
} )
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
const uniqueValidCards = Array . from ( latestCardsMap . values ( ) )
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// Map to track the most recent card per minterName
const mostRecentCardsMap = new Map ( )
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
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
2024-12-31 05:39:18 +00:00
2025-01-02 21:27:31 +00:00
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 } )
}
2024-12-19 03:39:15 +00:00
}
2025-01-02 21:27:31 +00:00
} 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 } )
}
} )
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// Convert the map into an array of final cards
const finalCards = Array . from ( mostRecentCardsMap . values ( ) ) ;
2024-12-27 19:49:07 +00:00
2025-01-02 21:27:31 +00:00
// 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 = ""
// Display skeleton cards immediately
finalCards . forEach ( ( { card } ) => {
const skeletonHTML = createSkeletonCardHTML ( card . identifier )
encryptedCardsContainer . insertAdjacentHTML ( "beforeend" , skeletonHTML )
} )
2025-01-01 03:48:34 +00:00
2025-01-02 21:27:31 +00:00
// Fetch poll results and update each card
await Promise . all (
finalCards . 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 } ` )
2025-01-01 03:48:34 +00:00
removeSkeleton ( card . identifier )
return
}
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
// 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
)
replaceSkeleton ( card . identifier , finalCardHTML )
} catch ( error ) {
console . error ( ` Error finalizing card ${ card . identifier } : ` , error )
removeSkeleton ( card . identifier )
}
} )
)
2024-12-19 03:39:15 +00:00
} catch ( error ) {
2024-12-31 05:39:18 +00:00
console . error ( "Error loading cards:" , error )
encryptedCardsContainer . innerHTML = "<p>Failed to load cards.</p>"
2024-12-19 03:39:15 +00:00
}
2024-12-31 05:39:18 +00:00
}
2024-12-19 03:39:15 +00:00
2025-01-02 21:27:31 +00:00
2025-01-01 03:48:34 +00:00
//TODO verify that this actually isn't necessary. if not, remove it.
// const removeEncryptedSkeleton = (cardIdentifier) => {
// const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
// if (encryptedSkeletonCard) {
// encryptedSkeletonCard.remove(); // Remove the skeleton silently
// }
// }
2024-12-19 03:39:15 +00:00
2025-01-01 03:48:34 +00:00
// const replaceEncryptedSkeleton = (cardIdentifier, htmlContent) => {
// const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
// if (encryptedSkeletonCard) {
// encryptedSkeletonCard.outerHTML = htmlContent;
// }
// }
2024-12-19 03:39:15 +00:00
// 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%;" > < / d i v >
< div style = "margin-left: 10px;" >
< div style = "width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;" > < / d i v >
< div style = "width: 80px; height: 15px; background-color: #ddd;" > < / d i v >
< / d i v >
< / d i v >
< div style = "margin-top: 10px;" >
< div style = "width: 100%; height: 40px; background-color: #eee;" > < / d i v >
< / d i v >
< / d i v >
2024-12-31 05:39:18 +00:00
`
}
2024-12-19 03:39:15 +00:00
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
2025-01-01 03:48:34 +00:00
const fetchExistingEncryptedCard = async ( minterName , existingIdentifier ) => {
try {
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : minterName ,
service : "MAIL_PRIVATE" ,
identifier : existingIdentifier ,
encoding : "base64"
} )
2024-12-19 03:39:15 +00:00
2025-01-01 03:48:34 +00:00
const decryptedCardData = await decryptAndParseObject ( cardDataResponse )
console . log ( "Full card data fetched successfully:" , decryptedCardData )
2024-12-19 03:39:15 +00:00
2025-01-01 03:48:34 +00:00
return decryptedCardData
2024-12-19 03:39:15 +00:00
} catch ( error ) {
2025-01-01 21:47:45 +00:00
console . error ( "Error fetching existing card:" , error )
2024-12-31 05:39:18 +00:00
return null
2024-12-19 03:39:15 +00:00
}
2024-12-31 05:39:18 +00:00
}
2024-12-19 03:39:15 +00:00
// 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" &&
2025-01-02 21:27:31 +00:00
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.
2024-12-19 03:39:15 +00:00
card . created
2024-12-31 05:39:18 +00:00
)
2024-12-19 03:39:15 +00:00
}
// Load existing card data passed, into the form for editing -------------------------------------
2025-01-01 03:48:34 +00:00
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
2024-12-19 03:39:15 +00:00
2024-12-31 05:39:18 +00:00
const linksContainer = document . getElementById ( "links-container" )
2024-12-19 03:39:15 +00:00
linksContainer . innerHTML = "" ; // Clear previous links
2025-01-01 03:48:34 +00:00
decryptedCardData . links . forEach ( link => {
2024-12-31 05:39:18 +00:00
const linkInput = document . createElement ( "input" )
linkInput . type = "text"
linkInput . className = "card-link"
linkInput . value = link
linksContainer . appendChild ( linkInput )
} )
2024-12-19 03:39:15 +00:00
}
}
const validateMinterName = async ( minterName ) => {
try {
const nameInfo = await getNameInfo ( minterName )
const name = nameInfo . name
return name
} catch ( error ) {
console . error ( ` extracting name from name info: ${ minterName } failed. ` , error )
2025-01-07 02:43:54 +00:00
return null
2024-12-19 03:39:15 +00:00
}
}
2024-12-24 08:27:17 +00:00
const publishEncryptedCard = async ( isTopicModePassed = false ) => {
// If the user wants it to be a topic, we set global isTopic = true, else false
2024-12-29 06:49:18 +00:00
isTopic = isTopicModePassed || isTopic
2024-12-24 08:27:17 +00:00
2024-12-31 05:39:18 +00:00
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 ( )
2024-12-19 03:39:15 +00:00
const links = Array . from ( document . querySelectorAll ( ".card-link" ) )
. map ( input => input . value . trim ( ) )
2024-12-31 05:39:18 +00:00
. filter ( link => link . startsWith ( "qortal://" ) )
2024-12-19 03:39:15 +00:00
2024-12-24 08:27:17 +00:00
// Basic validation
2024-12-19 03:39:15 +00:00
if ( ! header || ! content ) {
2024-12-31 05:39:18 +00:00
alert ( "Header and Content are required!" )
return
2024-12-19 03:39:15 +00:00
}
2024-12-31 05:39:18 +00:00
let publishedMinterName = minterNameInput
2024-12-19 03:39:15 +00:00
2024-12-24 08:27:17 +00:00
// If not topic mode, validate the user actually entered a valid Minter name
if ( ! isTopic ) {
2024-12-29 06:49:18 +00:00
publishedMinterName = await validateMinterName ( minterNameInput )
2024-12-24 08:27:17 +00:00
if ( ! publishedMinterName ) {
2025-01-01 03:48:34 +00:00
alert ( ` " ${ minterNameInput } " doesn't seem to be a valid name. Please check or use topic mode. ` )
2024-12-29 06:49:18 +00:00
return
2024-12-24 08:27:17 +00:00
}
// Also check for existing card if not topic
2025-01-01 03:48:34 +00:00
if ( ! isUpdateCard && existingCardMinterNames . some ( item => item . minterName === publishedMinterName ) ) {
const duplicateCardData = existingCardMinterNames . find ( item => item . minterName === publishedMinterName )
2024-12-24 08:27:17 +00:00
const updateCard = confirm (
2025-01-01 03:48:34 +00:00
` Minter Name: ${ publishedMinterName } already has a card. Duplicate name-based cards are not allowed. You can OVERWRITE it or Cancel publishing. UPDATE CARD? `
2024-12-29 06:49:18 +00:00
)
2024-12-24 08:27:17 +00:00
if ( updateCard ) {
2025-01-01 03:48:34 +00:00
existingEncryptedCardIdentifier = duplicateCardData . identifier
isUpdateCard = true
2024-12-24 08:27:17 +00:00
} else {
2024-12-29 06:49:18 +00:00
return
2024-12-19 03:39:15 +00:00
}
}
}
2024-12-24 08:27:17 +00:00
// Determine final card identifier
2025-01-02 21:27:31 +00:00
const currentTimestamp = Date . now ( )
2024-12-24 08:27:17 +00:00
const newCardIdentifier = isTopic
? ` ${ encryptedCardIdentifierPrefix } -TOPIC- ${ await uid ( ) } `
2025-01-02 21:27:31 +00:00
: ` ${ encryptedCardIdentifierPrefix } -NC- ${ currentTimestamp } - ${ await uid ( ) } `
2024-12-19 03:39:15 +00:00
2025-01-01 03:48:34 +00:00
const cardIdentifier = isUpdateCard ? existingEncryptedCardIdentifier : newCardIdentifier
2024-12-24 08:27:17 +00:00
// Build cardData
2024-12-29 06:49:18 +00:00
const pollName = ` ${ cardIdentifier } -poll `
2024-12-19 03:39:15 +00:00
const cardData = {
2024-12-24 08:27:17 +00:00
minterName : publishedMinterName ,
2024-12-19 03:39:15 +00:00
header ,
content ,
links ,
creator : userState . accountName ,
timestamp : Date . now ( ) ,
poll : pollName ,
2024-12-24 08:27:17 +00:00
topicMode : isTopic
2024-12-29 06:49:18 +00:00
}
2024-12-19 03:39:15 +00:00
2024-12-24 08:27:17 +00:00
try {
// Convert to base64 or fallback
2024-12-31 05:39:18 +00:00
let base64CardData = await objectToBase64 ( cardData )
2024-12-19 03:39:15 +00:00
if ( ! base64CardData ) {
2024-12-31 05:39:18 +00:00
base64CardData = btoa ( JSON . stringify ( cardData ) )
2024-12-19 03:39:15 +00:00
}
2024-12-24 08:27:17 +00:00
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
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
}
2024-12-24 08:27:17 +00:00
2024-12-19 03:39:15 +00:00
await qortalRequest ( {
action : "PUBLISH_QDN_RESOURCE" ,
name : userState . accountName ,
service : "MAIL_PRIVATE" ,
identifier : cardIdentifier ,
data64 : base64CardData ,
encrypt : true ,
publicKeys : verifiedAdminPublicKeys
2024-12-29 06:49:18 +00:00
} )
2024-12-19 03:39:15 +00:00
2024-12-24 08:27:17 +00:00
// Possibly create a poll if it’ s a brand new card
2025-01-01 03:48:34 +00:00
if ( ! isUpdateCard ) {
2024-12-24 08:27:17 +00:00
await qortalRequest ( {
action : "CREATE_POLL" ,
pollName ,
pollDescription : ` Admin Board Poll Published By ${ userState . accountName } ` ,
pollOptions : [ "Yes, No" ] ,
pollOwnerAddress : userState . accountAddress
2024-12-29 06:49:18 +00:00
} )
alert ( "Card and poll published successfully!" )
2024-12-24 08:27:17 +00:00
} else {
alert ( "Card updated successfully! (No poll updates possible currently...)" ) ;
2024-12-19 03:39:15 +00:00
}
2024-12-31 05:39:18 +00:00
document . getElementById ( "publish-card-form" ) . reset ( )
document . getElementById ( "publish-card-view" ) . style . display = "none"
document . getElementById ( "encrypted-cards-container" ) . style . display = "flex"
2024-12-24 08:27:17 +00:00
isTopic = false ; // reset global
2024-12-19 03:39:15 +00:00
} catch ( error ) {
2024-12-31 05:39:18 +00:00
console . error ( "Error publishing card or poll:" , error )
alert ( "Failed to publish card and poll." )
2024-12-19 03:39:15 +00:00
}
2024-12-31 05:39:18 +00:00
}
2024-12-24 08:27:17 +00:00
2024-12-19 03:39:15 +00:00
2024-12-21 06:07:18 +00:00
const getEncryptedCommentCount = async ( cardIdentifier ) => {
2024-12-20 05:28:36 +00:00
try {
2024-12-28 07:04:16 +00:00
const response = await searchSimple ( 'MAIL_PRIVATE' , ` comment- ${ cardIdentifier } ` , '' , 0 )
2024-12-29 06:49:18 +00:00
2024-12-28 07:04:16 +00:00
return Array . isArray ( response ) ? response . length : 0
2024-12-20 05:28:36 +00:00
} catch ( error ) {
2024-12-28 07:04:16 +00:00
console . error ( ` Error fetching comment count for ${ cardIdentifier } : ` , error )
return 0
2024-12-20 05:28:36 +00:00
}
2024-12-29 06:49:18 +00:00
}
2024-12-20 05:28:36 +00:00
2024-12-19 03:39:15 +00:00
// Post a comment on a card. ---------------------------------
const postEncryptedComment = async ( cardIdentifier ) => {
2024-12-29 06:49:18 +00:00
const commentInput = document . getElementById ( ` new-comment- ${ cardIdentifier } ` )
const commentText = commentInput . value . trim ( )
2024-12-19 03:39:15 +00:00
if ( ! commentText ) {
2024-12-29 06:49:18 +00:00
alert ( 'Comment cannot be empty!' )
return
2024-12-19 03:39:15 +00:00
}
2024-12-20 05:28:36 +00:00
const postTimestamp = Date . now ( )
2024-12-19 03:39:15 +00:00
const commentData = {
content : commentText ,
creator : userState . accountName ,
timestamp : postTimestamp ,
2024-12-29 06:49:18 +00:00
}
const commentIdentifier = ` comment- ${ cardIdentifier } - ${ await uid ( ) } `
2024-12-19 03:39:15 +00:00
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
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 ( )
2024-12-19 03:39:15 +00:00
adminPublicKeys = verifiedAdminPublicKeys
2024-12-24 08:27:17 +00:00
}
2024-12-19 03:39:15 +00:00
try {
2024-12-29 06:49:18 +00:00
const base64CommentData = await objectToBase64 ( commentData )
2024-12-19 03:39:15 +00:00
if ( ! base64CommentData ) {
2024-12-29 06:49:18 +00:00
console . log ( ` initial base64 object creation with objectToBase64 failed, using btoa... ` )
base64CommentData = btoa ( JSON . stringify ( commentData ) )
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
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 = ''
2024-12-19 03:39:15 +00:00
} catch ( error ) {
2024-12-29 06:49:18 +00:00
console . error ( 'Error posting comment:' , error )
alert ( 'Failed to post comment.' )
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
}
2024-12-19 03:39:15 +00:00
//Fetch the comments for a card with passed card identifier ----------------------------
const fetchEncryptedComments = async ( cardIdentifier ) => {
try {
2024-12-31 05:39:18 +00:00
const response = await searchSimple ( 'MAIL_PRIVATE' , ` comment- ${ cardIdentifier } ` , '' , 0 , 0 , '' , false )
2024-12-28 07:04:16 +00:00
if ( response ) {
2025-01-01 21:47:45 +00:00
return response
2024-12-28 07:04:16 +00:00
}
2024-12-19 03:39:15 +00:00
} catch ( error ) {
2024-12-29 06:49:18 +00:00
console . error ( ` Error fetching comments for ${ cardIdentifier } : ` , error )
return [ ]
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
}
2024-12-19 03:39:15 +00:00
const displayEncryptedComments = async ( cardIdentifier ) => {
try {
2024-12-29 06:49:18 +00:00
const comments = await fetchEncryptedComments ( cardIdentifier )
const commentsContainer = document . getElementById ( ` comments-container- ${ cardIdentifier } ` )
2025-01-01 21:47:45 +00:00
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 = ` <span style="color: ${ badgeColor } ; font-weight: bold; margin-left: 0.5em;">(Admin)</span> `
} 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 `
< div class = "comment" style = "border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: ${commentColor};" >
< p >
< strong > < u > $ { decryptedCommentData . creator } < / u > < / s t r o n g >
$ { adminBadge }
< / p >
< p > $ { decryptedCommentData . content } < / p >
< p > < i > $ { timestamp } < / i > < / p >
< / d i v >
`
} catch ( err ) {
console . error ( ` Error processing comment ${ comment . identifier } : ` , err )
return null // Skip this comment if it fails
}
2024-12-29 06:49:18 +00:00
} )
2025-01-01 21:47:45 +00:00
)
2024-12-19 03:39:15 +00:00
2025-01-01 21:47:45 +00:00
// Add all comments to the container
commentHTMLArray
. filter ( ( html ) => html !== null ) // Filter out failed comments
. forEach ( ( commentHTML ) => {
commentsContainer . insertAdjacentHTML ( 'beforeend' , commentHTML )
} )
2024-12-19 03:39:15 +00:00
} catch ( error ) {
2024-12-29 06:49:18 +00:00
console . error ( ` Error displaying comments (or no comments) for ${ cardIdentifier } : ` , error )
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
}
2024-12-19 03:39:15 +00:00
2025-01-01 21:47:45 +00:00
2024-12-19 03:39:15 +00:00
const toggleEncryptedComments = async ( cardIdentifier ) => {
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
const commentsSection = document . getElementById ( ` comments-section- ${ cardIdentifier } ` )
const commentButton = document . getElementById ( ` comment-button- ${ cardIdentifier } ` )
2024-12-29 06:49:18 +00:00
if ( ! commentsSection || ! commentButton ) return
const count = commentButton . dataset . commentCount ;
const isHidden = ( commentsSection . style . display === 'none' || ! commentsSection . style . display )
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
if ( isHidden ) {
// Show comments
2024-12-29 06:49:18 +00:00
commentButton . textContent = "LOADING..."
await displayEncryptedComments ( cardIdentifier )
commentsSection . style . display = 'block'
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
// Change the button text to 'HIDE COMMENTS'
2024-12-29 06:49:18 +00:00
commentButton . textContent = 'HIDE COMMENTS'
2024-12-19 03:39:15 +00:00
} else {
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
// Hide comments
2024-12-29 06:49:18 +00:00
commentsSection . style . display = 'none'
commentButton . textContent = ` COMMENTS ( ${ count } ) `
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
}
2024-12-19 03:39:15 +00:00
const createLinkDisplayModal = async ( ) => {
const modalHTML = `
2024-12-29 06:49:18 +00:00
< div id = "links-modal" style = "display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;" >
2024-12-19 03:39:15 +00:00
< div style = "position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;" >
2024-12-29 06:49:18 +00:00
< iframe id = "links-modalContent" src = "" style = "width: 100%; height: 100%; border: none;" > < / i f r a m e >
2024-12-19 03:39:15 +00:00
< button onclick = "closeLinkDisplayModal()" style = "position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;" > Close < / b u t t o n >
< / d i v >
< / d i v >
2024-12-29 06:49:18 +00:00
`
document . body . insertAdjacentHTML ( 'beforeend' , modalHTML )
2024-12-19 03:39:15 +00:00
}
// Function to open the modal
const openLinkDisplayModal = async ( link ) => {
const processedLink = await processQortalLinkForRendering ( link ) // Process the link to replace `qortal://` for rendering in modal
2025-01-01 21:47:45 +00:00
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
2024-12-19 03:39:15 +00:00
}
// Function to close the modal
const closeLinkDisplayModal = async ( ) => {
2025-01-01 21:47:45 +00:00
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
2024-12-19 03:39:15 +00:00
}
const processQortalLinkForRendering = async ( link ) => {
if ( link . startsWith ( 'qortal://' ) ) {
2024-12-29 06:49:18 +00:00
const match = link . match ( /^qortal:\/\/([^/]+)(\/.*)?$/ )
2024-12-19 03:39:15 +00:00
if ( match ) {
2025-01-01 21:47:45 +00:00
const firstParam = match [ 1 ] . toUpperCase ( )
2024-12-29 06:49:18 +00:00
const remainingPath = match [ 2 ] || ""
const themeColor = window . _qdnTheme || 'default' // Fallback to 'default' if undefined
2024-12-21 06:07:18 +00:00
// Simulating async operation if needed
2024-12-29 06:49:18 +00:00
await new Promise ( resolve => setTimeout ( resolve , 10 ) )
2024-12-21 06:07:18 +00:00
2024-12-29 06:49:18 +00:00
return ` /render/ ${ firstParam } ${ remainingPath } ?theme= ${ themeColor } `
2024-12-21 06:07:18 +00:00
}
}
2024-12-29 06:49:18 +00:00
return link
}
2024-12-21 06:07:18 +00:00
2025-01-07 02:43:54 +00:00
const checkAndDisplayRemoveActions = async ( adminYes , creator , cardIdentifier ) => {
const latestBlockInfo = await getLatestBlockInfo ( )
const isBlockPassed = latestBlockInfo . height >= GROUP _APPROVAL _FEATURE _TRIGGER _HEIGHT
let minAdminCount = 9
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 } ` )
} 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 } ` )
}
//TODO verify the above functionality to calculate 40% of MINTER group admins, and use that for minAdminCount
if ( adminYes >= minAdminCount && userState . isMinterAdmin && ! isBlockPassed ) {
const removeButtonHtml = createRemoveButtonHtml ( creator , cardIdentifier )
return removeButtonHtml
}
return ''
}
const createRemoveButtonHtml = ( name , cardIdentifier ) => {
return `
< div id = "remove-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button onclick = "handleKickMinter('${name}')"
style = "padding: 10px; background: rgb(134, 80, 4); color: white; border: none; cursor: pointer; border-radius: 5px;"
onmouseover = "this.style.backgroundColor='rgb(47, 28, 11) '"
onmouseout = "this.style.backgroundColor='rgb(134, 80, 4) '" >
KICK Minter
< / b u t t o n >
< button onclick = "handleBanMinter('${name}')"
style = "padding: 10px; background:rgb(93, 7, 7); color: white; border: none; cursor: pointer; border-radius: 5px;"
onmouseover = "this.style.backgroundColor='rgb(39, 9, 9) '"
onmouseout = "this.style.backgroundColor='rgb(93, 7, 7) '" >
BAN Minter
< / b u t t o n >
< / d i v >
`
}
const handleKickMinter = async ( minterName ) => {
2024-12-21 06:07:18 +00:00
try {
2025-01-07 02:43:54 +00:00
// Optional block check
const { height : currentHeight } = await getLatestBlockInfo ( )
if ( currentHeight <= GROUP _APPROVAL _FEATURE _TRIGGER _HEIGHT ) {
console . log ( ` block height is under the removal featureTrigger height ` )
}
// Get the minter address from name info
const minterInfo = await getNameInfo ( minterName )
const minterAddress = minterInfo ? . owner
if ( ! minterAddress ) {
alert ( ` No valid address found for minter name: ${ minterName } ` )
return
}
// The admin public key
const adminPublicKey = await getPublicKeyByName ( userState . accountName )
// Create the raw remove transaction
const rawKickTransaction = await createGroupKickTransaction ( minterAddress , adminPublicKey , 694 , minterAddress )
2024-12-29 06:49:18 +00:00
2025-01-07 02:43:54 +00:00
// Sign the transaction
const signedKickTransaction = await qortalRequest ( {
action : "SIGN_TRANSACTION" ,
unsignedBytes : rawKickTransaction
} )
// Process the transaction
const processResponse = await processTransaction ( signedKickTransaction )
if ( processResponse ? . status === "OK" ) {
alert ( ` ${ minterName } 's KICK transaction has been SUCCESSFULLY PROCESSED. Please WAIT FOR CONFIRMATION... ` )
2024-12-21 06:07:18 +00:00
} else {
2025-01-07 02:43:54 +00:00
alert ( "Failed to process the removal transaction." )
2024-12-19 03:39:15 +00:00
}
2024-12-29 06:49:18 +00:00
2024-12-21 06:07:18 +00:00
} catch ( error ) {
2025-01-07 02:43:54 +00:00
console . error ( "Error removing minter:" , error )
alert ( "Error removing minter. Please try again." )
2024-12-19 03:39:15 +00:00
}
}
2025-01-07 02:43:54 +00:00
const handleBanMinter = async ( minterName ) => {
try {
const { height : currentHeight } = await getLatestBlockInfo ( )
if ( currentHeight <= GROUP _APPROVAL _FEATURE _TRIGGER _HEIGHT ) {
console . log ( ` block height is under the removal featureTrigger height ` )
}
const minterInfo = await getNameInfo ( minterName )
const minterAddress = minterInfo ? . owner
if ( ! minterAddress ) {
alert ( ` No valid address found for minter name: ${ minterName } ` )
return
}
const adminPublicKey = await getPublicKeyByName ( userState . accountName )
const rawBanTransaction = await createGroupBanTransaction ( minterAddress , adminPublicKey , 694 , minterAddress )
const signedBanTransaction = await qortalRequest ( {
action : "SIGN_TRANSACTION" ,
unsignedBytes : rawBanTransaction
} )
const processResponse = await processTransaction ( signedBanTransaction )
if ( processResponse ? . status === "OK" ) {
alert ( ` ${ minterName } 's BAN transaction has been SUCCESSFULLY PROCESSED. Please WAIT FOR CONFIRMATION... ` )
} else {
alert ( "Failed to process the removal transaction." )
}
} catch ( error ) {
console . error ( "Error removing minter:" , error )
alert ( "Error removing minter. Please try again." )
}
}
2024-12-29 06:49:18 +00:00
2024-12-19 03:39:15 +00:00
// Create the overall Minter Card HTML -----------------------------------------------
2024-12-20 05:28:36 +00:00
const createEncryptedCardHTML = async ( cardData , pollResults , cardIdentifier , commentCount ) => {
2024-12-24 08:27:17 +00:00
const { minterName , header , content , links , creator , timestamp , poll , topicMode } = cardData
const formattedDate = new Date ( timestamp ) . toLocaleString ( )
const minterAvatar = ! topicMode ? await getMinterAvatar ( minterName ) : null
2024-12-21 06:07:18 +00:00
const creatorAvatar = await getMinterAvatar ( creator )
2024-12-19 03:39:15 +00:00
const linksHTML = links . map ( ( link , index ) => `
< button onclick = "openLinkDisplayModal('${link}')" >
$ { ` Link ${ index + 1 } - ${ link } ` }
< / b u t t o n >
2024-12-24 08:27:17 +00:00
` ).join("")
const isUndefinedUser = ( minterName === 'undefined' )
2024-12-19 03:39:15 +00:00
2024-12-24 08:27:17 +00:00
const hasTopicMode = Object . prototype . hasOwnProperty . call ( cardData , 'topicMode' )
2024-12-29 06:49:18 +00:00
2024-12-24 08:27:17 +00:00
let showTopic = false
2024-12-29 06:49:18 +00:00
2024-12-24 08:27:17 +00:00
if ( hasTopicMode ) {
2024-12-29 06:49:18 +00:00
const modeVal = cardData . topicMode
2024-12-24 08:27:17 +00:00
showTopic = ( modeVal === true || modeVal === 'true' )
} else {
if ( ! isUndefinedUser ) {
showTopic = false
}
}
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
2024-12-24 08:27:17 +00:00
const cardColorCode = showTopic ? '#0e1b15' : '#151f28'
const minterOrTopicHtml = ( ( showTopic ) || ( isUndefinedUser ) ) ? `
< div class = "support-header" > < h5 > REGARDING ( Topic ) : < / h 5 > < / d i v >
< h3 > $ { minterName } < / h 3 > ` :
`
< div class = "support-header" > < h5 > REGARDING ( Name ) : < / h 5 > < / d i v >
$ { minterAvatar }
< h3 > $ { minterName } < / h 3 > `
const minterGroupMembers = await fetchMinterGroupMembers ( )
const minterAdmins = await fetchMinterGroupAdmins ( )
2025-01-01 21:47:45 +00:00
const { adminYes = 0 , adminNo = 0 , minterYes = 0 , minterNo = 0 , totalYes = 0 , totalNo = 0 , totalYesWeight = 0 , totalNoWeight = 0 , detailsHtml } = await processPollData ( pollResults , minterGroupMembers , minterAdmins , creator , cardIdentifier )
2025-01-07 02:43:54 +00:00
2024-12-29 06:49:18 +00:00
createModal ( 'links' )
createModal ( 'poll-details' )
2025-01-07 02:43:54 +00:00
let showRemoveHtml
const verifiedName = await validateMinterName ( minterName )
if ( verifiedName ) {
console . log ( ` name is validated, utilizing for removal features... ${ verifiedName } ` )
const removeActionsHtml = await checkAndDisplayRemoveActions ( adminYes , verifiedName , cardIdentifier )
showRemoveHtml = removeActionsHtml
} else {
console . log ( ` name could not be validated, assuming topic card (or some other issue with name validation) for removalActions ` )
showRemoveHtml = ''
}
2024-12-19 03:39:15 +00:00
return `
2024-12-24 08:27:17 +00:00
< div class = "admin-card" style = "background-color: ${cardColorCode}" >
2024-12-19 03:39:15 +00:00
< div class = "minter-card-header" >
2024-12-21 06:07:18 +00:00
< h2 class = "support-header" > Created By : < / h 2 >
$ { creatorAvatar }
2024-12-19 03:39:15 +00:00
< h2 > $ { creator } < / h 2 >
2024-12-24 08:27:17 +00:00
$ { minterOrTopicHtml }
2024-12-19 03:39:15 +00:00
< p > $ { header } < / p >
< / d i v >
< div class = "info" >
$ { content }
< / d i v >
2024-12-21 06:07:18 +00:00
< div class = "support-header" > < h5 > LINKS < / h 5 > < / d i v >
2024-12-19 03:39:15 +00:00
< div class = "info-links" >
$ { linksHTML }
< / d i v >
2024-12-21 06:07:18 +00:00
< div class = "results-header support-header" > < h5 > CURRENT RESULTS < / h 5 > < / d i v >
2024-12-19 03:39:15 +00:00
< div class = "minter-card-results" >
2024-12-29 06:49:18 +00:00
< button onclick = "togglePollDetails('${cardIdentifier}')" > Display Poll Details < / b u t t o n >
< div id = "poll-details-${cardIdentifier}" style = "display: none;" >
$ { detailsHtml }
< / d i v >
2025-01-07 02:43:54 +00:00
$ { showRemoveHtml }
2024-12-19 03:39:15 +00:00
< div class = "admin-results" >
2024-12-21 06:07:18 +00:00
< span class = "admin-yes" > Admin Support : $ { adminYes } < / s p a n >
< span class = "admin-no" > Admin Against : $ { adminNo } < / s p a n >
2024-12-19 03:39:15 +00:00
< / d i v >
< div class = "minter-results" >
2024-12-21 06:07:18 +00:00
< span class = "minter-yes" > Supporting Weight $ { totalYesWeight } < / s p a n >
< span class = "minter-no" > Denial Weight $ { totalNoWeight } < / s p a n >
2024-12-19 03:39:15 +00:00
< / d i v >
< / d i v >
2024-12-24 08:27:17 +00:00
< div class = "support-header" > < h5 > ACTIONS FOR < / h 5 > < h 5 s t y l e = " c o l o r : # f f a e 4 2 ; " > $ { m i n t e r N a m e } < / h 5 >
2024-12-21 06:07:18 +00:00
< p style = "color: #c7c7c7; font-size: .65rem; margin-top: 1vh" > ( click COMMENTS button to open / close card comments ) < / p >
< / d i v >
2024-12-19 03:39:15 +00:00
< div class = "actions" >
< div class = "actions-buttons" >
2024-12-24 08:27:17 +00:00
< button class = "yes" onclick = "voteYesOnPoll('${poll}')" > YES < / b u t t o n >
This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.
2024-12-27 04:06:51 +00:00
< button id = "comment-button-${cardIdentifier}" data - comment - count = "${commentCount}" class = "comment" onclick = "toggleEncryptedComments('${cardIdentifier}')" > COMMENTS ( $ { commentCount } ) < / b u t t o n >
2024-12-24 08:27:17 +00:00
< button class = "no" onclick = "voteNoOnPoll('${poll}')" > NO < / b u t t o n >
2024-12-19 03:39:15 +00:00
< / d i v >
< / d i v >
< div id = "comments-section-${cardIdentifier}" class = "comments-section" style = "display: none; margin-top: 20px;" >
< div id = "comments-container-${cardIdentifier}" class = "comments-container" > < / d i v >
2024-12-21 06:07:18 +00:00
< textarea id = "new-comment-${cardIdentifier}" placeholder = "Input your comment..." style = "width: 100%; margin-top: 10px;" > < / t e x t a r e a >
2024-12-19 03:39:15 +00:00
< button onclick = "postEncryptedComment('${cardIdentifier}')" > Post Comment < / b u t t o n >
< / d i v >
2024-12-21 06:07:18 +00:00
< p style = "font-size: 0.75rem; margin-top: 1vh; color: #4496a1" > By : $ { creator } - $ { formattedDate } < / p >
2024-12-19 03:39:15 +00:00
< / d i v >
2024-12-24 08:27:17 +00:00
`
2024-12-19 03:39:15 +00:00
}