2024-12-17 22:24:40 -08:00
// // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
2025-01-01 13:47:45 -08:00
const testMode = false
2025-01-21 21:23:25 -08:00
const minterCardIdentifierPrefix = "Minter-board-card"
2025-01-01 13:47:45 -08:00
let isExistingCard = false
let existingCardData = { }
let existingCardIdentifier = { }
2025-01-04 20:28:26 -08:00
const MIN _ADMIN _YES _VOTES = 9 ;
2025-01-21 21:23:25 -08:00
const GROUP _APPROVAL _FEATURE _TRIGGER _HEIGHT = 2012800 //TODO update this to correct featureTrigger height when known, either that, or pull from core.
2025-01-13 15:53:20 -08:00
let featureTriggerPassed = false
2025-01-04 20:28:26 -08:00
let isApproved = false
2024-12-11 14:40:32 -08:00
2025-01-15 19:14:16 -08:00
2024-12-12 17:23:34 -08:00
const loadMinterBoardPage = async ( ) => {
2024-12-11 14:40:32 -08:00
// Clear existing content on the page
2025-01-01 13:47:45 -08:00
const bodyChildren = document . body . children
2024-12-11 14:40:32 -08:00
for ( let i = bodyChildren . length - 1 ; i >= 0 ; i -- ) {
const child = bodyChildren [ i ] ;
if ( ! child . classList . contains ( "menu" ) ) {
2025-01-01 13:47:45 -08:00
child . remove ( )
2024-12-11 14:40:32 -08:00
}
}
// Add the "Minter Board" content
2025-01-01 13:47:45 -08:00
const mainContent = document . createElement ( "div" )
2024-12-24 00:27:17 -08:00
const publishButtonColor = '#527c9d'
const minterBoardNameColor = '#527c9d'
2024-12-11 14:40:32 -08:00
mainContent . innerHTML = `
< div class = "minter-board-main" style = "padding: 20px; text-align: center;" >
2024-12-20 22:07:18 -08:00
< h1 style = "color: ${minterBoardNameColor};" > Minter Board < / h 1 >
< p style = "font-size: 1.25em;" > Publish a Minter Card with Information , and obtain and view the support of the community . Welcome to the Minter Board ! < / p >
< button id = "publish-card-button" class = "publish-card-button" style = "margin: 20px; padding: 10px; background-color: ${publishButtonColor}" > Publish Minter Card < / b u t t o n >
2024-12-16 19:53:37 -08:00
< button id = "refresh-cards-button" class = "refresh-cards-button" style = "padding: 10px;" > Refresh Cards < / b u t t o n >
2025-01-29 19:12:30 -08:00
< select id = "sort-select" style = "margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(38, 106, 106); background-color: black;" >
2025-01-22 01:52:41 -05:00
< option value = "newest" selected > Sort by Date < / o p t i o n >
< option value = "name" > Sort by Name < / o p t i o n >
< option value = "recent-comments" > Newest Comments < / o p t i o n >
< option value = "least-votes" > Least Votes < / o p t i o n >
< option value = "most-votes" > Most Votes < / o p t i o n >
< / s e l e c t >
2025-02-03 09:34:23 -08:00
< span id = "board-card-counter" style = "font-size: 1rem; color: #999ccc; padding: 0.5em;" > < / s p a n >
2025-01-29 19:12:30 -08:00
< select id = "time-range-select" style = "margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;" >
2025-01-27 21:03:13 -08:00
< option value = "0" > Show All < / o p t i o n >
< option value = "1" > Last 1 day < / o p t i o n >
< option value = "7" > Last 7 days < / o p t i o n >
< option value = "30" selected > Last 30 days < / o p t i o n >
< option value = "90" > Last 90 days < / o p t i o n >
< / s e l e c t >
2025-02-03 09:34:23 -08:00
< label style = "color:rgb(181, 181, 181); margin-left: 10px;" >
< input type = "checkbox" id = "show-existing-checkbox" / >
Show Existing Minter Cards ( history )
< / l a b e l >
2024-12-11 14:40:32 -08:00
< div id = "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;" >
2025-01-29 19:12:30 -08:00
< form id = "publish-card-form" class = "publish-card-form" >
2025-01-04 20:28:26 -08:00
< h3 > Create or Update Your Card < / h 3 >
2024-12-11 14:40:32 -08:00
< label for = "card-header" > Header : < / l a b e l >
< input type = "text" id = "card-header" maxlength = "100" placeholder = "Enter card header" required >
< label for = "card-content" > Content : < / l a b e l >
2024-12-28 22:49:18 -08:00
< textarea id = "card-content" placeholder = "Enter detailed information about why you would like to be a minter... the more the better, and links to things you have published on QDN will help a lot! Give the Minter Admins things to make decisions by!" required > < / t e x t a r e a >
2024-12-11 14:40:32 -08: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 >
2025-01-01 13:47:45 -08:00
`
document . body . appendChild ( mainContent )
2025-01-04 20:28:26 -08:00
createScrollToTopButton ( )
2024-12-11 14:40:32 -08:00
document . getElementById ( "publish-card-button" ) . addEventListener ( "click" , async ( ) => {
try {
2025-01-21 21:23:25 -08:00
const fetchedCard = await fetchExistingCard ( minterCardIdentifierPrefix )
2024-12-16 19:53:37 -08:00
if ( fetchedCard ) {
// An existing card is found
if ( testMode ) {
// In test mode, ask user what to do
2025-01-01 13:47:45 -08:00
const updateCard = confirm ( "A card already exists. Do you want to update it?" )
2024-12-16 19:53:37 -08:00
if ( updateCard ) {
2025-01-01 13:47:45 -08:00
isExistingCard = true
await loadCardIntoForm ( existingCardData )
alert ( "Edit your existing card and publish." )
2024-12-16 19:53:37 -08:00
} else {
2025-01-01 13:47:45 -08:00
alert ( "Test mode: You can now create a new card." )
isExistingCard = false
existingCardData = { }
document . getElementById ( "publish-card-form" ) . reset ( )
2024-12-16 19:53:37 -08:00
}
2024-12-11 14:40:32 -08:00
} else {
2024-12-16 19:53:37 -08:00
// Not in test mode, force editing
alert ( "A card already exists. Publishing of multiple cards is not allowed. Please update your card." ) ;
isExistingCard = true ;
2025-01-01 13:47:45 -08:00
await loadCardIntoForm ( existingCardData )
2024-12-11 14:40:32 -08:00
}
} else {
2024-12-16 19:53:37 -08:00
// No existing card found
2025-01-01 13:47:45 -08:00
console . log ( "No existing card found. Creating a new card." )
isExistingCard = false
2024-12-11 14:40:32 -08:00
}
2024-12-16 19:53:37 -08:00
// Show the form
2025-01-01 13:47:45 -08:00
const publishCardView = document . getElementById ( "publish-card-view" )
2025-01-21 21:23:25 -08:00
publishCardView . style . display = "flex"
2025-01-01 13:47:45 -08:00
document . getElementById ( "cards-container" ) . style . display = "none"
2024-12-11 14:40:32 -08:00
} catch ( error ) {
2025-01-01 13:47:45 -08:00
console . error ( "Error checking for existing card:" , error )
alert ( "Failed to check for existing card. Please try again." )
2024-12-11 14:40:32 -08:00
}
2025-01-01 13:47:45 -08:00
} )
2024-12-11 14:40:32 -08:00
2024-12-16 19:53:37 -08:00
document . getElementById ( "refresh-cards-button" ) . addEventListener ( "click" , async ( ) => {
2025-01-01 13:47:45 -08:00
const cardsContainer = document . getElementById ( "cards-container" )
cardsContainer . innerHTML = "<p>Refreshing cards...</p>"
2025-01-21 21:23:25 -08:00
await loadCards ( minterCardIdentifierPrefix )
2025-01-01 13:47:45 -08:00
} )
2024-12-16 19:53:37 -08:00
2024-12-12 17:23:34 -08:00
document . getElementById ( "cancel-publish-button" ) . addEventListener ( "click" , async ( ) => {
2025-01-01 13:47:45 -08:00
const cardsContainer = document . getElementById ( "cards-container" )
2024-12-11 14:40:32 -08:00
cardsContainer . style . display = "flex" ; // Restore visibility
2025-01-01 13:47:45 -08:00
const publishCardView = document . getElementById ( "publish-card-view" )
2024-12-11 14:40:32 -08:00
publishCardView . style . display = "none" ; // Hide the publish form
2025-01-01 13:47:45 -08:00
} )
2024-12-11 14:40:32 -08:00
2024-12-12 17:23:34 -08:00
document . getElementById ( "add-link-button" ) . addEventListener ( "click" , async ( ) => {
2025-01-01 13:47:45 -08:00
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 )
} )
2024-12-11 14:40:32 -08:00
document . getElementById ( "publish-card-form" ) . addEventListener ( "submit" , async ( event ) => {
2025-01-01 13:47:45 -08:00
event . preventDefault ( )
2025-01-21 21:23:25 -08:00
await publishCard ( minterCardIdentifierPrefix )
2025-01-01 13:47:45 -08:00
} )
2025-01-22 01:52:41 -05:00
2025-01-27 21:03:13 -08:00
document . getElementById ( "time-range-select" ) . addEventListener ( "change" , async ( ) => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards ( minterCardIdentifierPrefix )
} )
2025-01-22 01:52:41 -05:00
document . getElementById ( "sort-select" ) . addEventListener ( "change" , async ( ) => {
// Re-load the cards whenever user chooses a new sort option.
2025-01-22 02:29:27 -05:00
await loadCards ( minterCardIdentifierPrefix )
2025-01-22 01:52:41 -05:00
} )
2025-02-03 09:34:23 -08:00
const showExistingCardsCheckbox = document . getElementById ( 'show-existing-checkbox' )
if ( showExistingCardsCheckbox ) {
showExistingCardsCheckbox . addEventListener ( 'change' , async ( event ) => {
await loadCards ( minterCardIdentifierPrefix )
} )
}
2025-01-13 15:53:20 -08:00
await featureTriggerCheck ( )
2025-01-21 21:23:25 -08:00
await loadCards ( minterCardIdentifierPrefix )
2024-12-11 14:40:32 -08:00
}
2025-01-29 19:12:30 -08: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-26 20:06:51 -08:00
const extractMinterCardsMinterName = async ( cardIdentifier ) => {
// Ensure the identifier starts with the prefix
2025-01-21 21:23:25 -08:00
if ( ( ! cardIdentifier . startsWith ( minterCardIdentifierPrefix ) ) && ( ! cardIdentifier . startsWith ( addRemoveIdentifierPrefix ) ) ) {
throw new Error ( 'minterCard does not match identifier check' )
}
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-26 20:06:51 -08:00
// Split the identifier into parts
2024-12-27 23:04:16 -08:00
const parts = cardIdentifier . split ( '-' )
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-26 20:06:51 -08:00
// Ensure the format has at least 3 parts
if ( parts . length < 3 ) {
2024-12-27 23:04:16 -08:00
throw new Error ( 'Invalid identifier format' )
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-26 20:06:51 -08:00
}
try {
2025-01-21 21:23:25 -08:00
if ( cardIdentifier . startsWith ( minterCardIdentifierPrefix ) ) {
2025-01-27 21:03:13 -08:00
const searchSimpleResults = await searchSimple ( 'BLOG_POST' , ` ${ cardIdentifier } ` , '' , 1 , 0 , '' , false , true )
2025-01-21 21:23:25 -08:00
const minterName = await searchSimpleResults . name
return minterName
} else if ( cardIdentifier . startsWith ( addRemoveIdentifierPrefix ) ) {
2025-01-27 21:03:13 -08:00
const searchSimpleResults = await searchSimple ( 'BLOG_POST' , ` ${ cardIdentifier } ` , '' , 1 , 0 , '' , false , true )
2025-01-21 21:23:25 -08:00
const publisherName = searchSimpleResults . name
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : publisherName ,
service : "BLOG_POST" ,
identifier : cardIdentifier ,
} )
2025-01-27 21:03:13 -08:00
let nameInvalid = false
2025-01-21 21:23:25 -08:00
const minterName = cardDataResponse . minterName
2025-01-27 21:03:13 -08:00
if ( minterName ) {
return minterName
} else {
nameInvalid = true
console . warn ( ` fuckery detected on identifier: ${ cardIdentifier } , hello dipshit Mythril!, name invalid? Name doesn't match publisher? Returning invalid flag + publisherName... ` )
return publisherName
}
2025-01-21 21:23:25 -08: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-26 20:06:51 -08:00
} catch ( error ) {
throw error
}
}
2025-01-27 21:03:13 -08:00
const groupAndLabelByIdentifier = ( allCards ) => {
// Group by identifier
const mapById = new Map ( )
allCards . forEach ( card => {
if ( ! mapById . has ( card . identifier ) ) {
mapById . set ( card . identifier , [ ] )
}
mapById . get ( card . identifier ) . push ( card )
} )
// For each identifier's group, sort oldest->newest so the first is "master"
const output = [ ]
for ( const [ identifier , group ] of mapById . entries ( ) ) {
group . sort ( ( a , b ) => {
const aTime = a . created || 0
const bTime = b . created || 0
return aTime - bTime // oldest first
} )
// Mark the first as master
group [ 0 ] . isMaster = true
// The rest are updates
for ( let i = 1 ; i < group . length ; i ++ ) {
group [ i ] . isMaster = 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-26 20:06:51 -08:00
}
2025-01-27 21:03:13 -08:00
// push them all to output
output . push ( ... group )
}
2025-01-21 21:23:25 -08:00
2025-01-27 21:03:13 -08:00
return output
}
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-26 20:06:51 -08:00
2025-01-27 21:03:13 -08:00
const groupByIdentifierOldestFirst = ( allCards ) => {
// map of identifier => array of cards
const mapById = new Map ( )
allCards . forEach ( card => {
if ( ! mapById . has ( card . identifier ) ) {
mapById . set ( card . identifier , [ ] )
2025-01-21 21:23:25 -08:00
}
2025-01-27 21:03:13 -08:00
mapById . get ( card . identifier ) . push ( card )
} )
// sort each group oldest->newest
for ( const [ identifier , group ] of mapById . entries ( ) ) {
group . sort ( ( a , b ) => {
const aTime = a . created || 0
const bTime = b . created || 0
return aTime - bTime // oldest first
} )
}
return mapById
}
const buildMinterNameGroups = async ( mapById ) => {
// We'll build an array of objects: { minterName, cards }
// Then we can combine any that share the same minterName.
const nameGroups = [ ]
for ( let [ identifier , group ] of mapById . entries ( ) ) {
// group[0] is the oldest => "master" card
let masterCard = group [ 0 ]
// Filter out any cards that are not published by the 'masterPublisher'
const masterPublisherName = masterCard . name
// Remove any cards in this identifier group that have a different publisherName
const filteredGroup = group . filter ( c => c . name === masterPublisherName )
// If filtering left zero cards, skip entire group
if ( ! filteredGroup . length ) {
console . warn ( ` All cards removed for identifier= ${ identifier } (different publishers). Skipping. ` )
2024-12-30 21:39:18 -08:00
continue
}
2025-01-27 21:03:13 -08:00
// Reassign group to the filtered version, then re-define masterCard
group = filteredGroup
masterCard = group [ 0 ] // oldest after filtering
// attempt to obtain minterName from the master card
let masterMinterName
try {
masterMinterName = await extractMinterCardsMinterName ( masterCard . identifier )
} catch ( err ) {
console . warn ( ` Skipping entire group ${ identifier } , no valid minterName from master ` , err )
2024-12-30 21:39:18 -08:00
continue
}
2025-01-27 21:03:13 -08:00
// Store an object with the minterName we extracted, plus all cards in that group
nameGroups . push ( {
minterName : masterMinterName ,
cards : group // includes the master & updates
} )
}
// Combine them: minterName => array of *all* cards from all matching groups
const combinedMap = new Map ( )
for ( const entry of nameGroups ) {
const mName = entry . minterName
if ( ! combinedMap . has ( mName ) ) {
combinedMap . set ( mName , [ ] )
2025-01-21 21:23:25 -08:00
}
2025-01-27 21:03:13 -08:00
combinedMap . get ( mName ) . push ( ... entry . cards )
}
2025-01-21 21:23:25 -08:00
2025-01-27 21:03:13 -08:00
return combinedMap
}
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-26 20:06:51 -08:00
2025-01-27 21:03:13 -08:00
const getNewestCardPerMinterName = ( combinedMap ) => {
// We'll produce an array of the newest card for each minterName, this will be utilized as the 'final filter' to display cards published/updated by unique minters.
const finalOutput = [ ]
for ( const [ mName , cardArray ] of combinedMap . entries ( ) ) {
// sort by updated or created, descending => newest first
cardArray . sort ( ( a , b ) => {
const aTime = a . updated || a . created || 0
const bTime = b . updated || b . created || 0
return bTime - aTime
} )
// newest is [0]
finalOutput . push ( cardArray [ 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-26 20:06:51 -08:00
}
2025-01-27 21:03:13 -08:00
// Then maybe globally sort them newest first
finalOutput . sort ( ( a , b ) => {
const aTime = a . updated || a . created || 0
const bTime = b . updated || b . created || 0
return bTime - aTime
} )
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-26 20:06:51 -08:00
2025-01-27 21:03:13 -08:00
return finalOutput
}
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-26 20:06:51 -08:00
2025-01-27 21:03:13 -08:00
const processMinterBoardCards = async ( allValidCards ) => {
// group by identifier, sorted oldest->newest
const mapById = groupByIdentifierOldestFirst ( allValidCards )
// build a map of minterName => all cards from those identifiers
const minterNameMap = await buildMinterNameGroups ( mapById )
// from that map, keep only the single newest card per minterName
const newestCards = getNewestCardPerMinterName ( minterNameMap )
// return final array of all newest cards
return newestCards
}
const processARBoardCards = async ( allValidCards ) => {
const mapById = groupByIdentifierOldestFirst ( allValidCards )
// build a map of minterName => all cards from those identifiers
const mapByName = await buildMinterNameGroups ( mapById )
// For each minterName group, we might want to sort them newest->oldest
const finalOutput = [ ]
for ( const [ minterName , group ] of mapByName . entries ( ) ) {
group . sort ( ( a , b ) => {
const aTime = a . updated || a . created || 0
const bTime = b . updated || b . created || 0
return bTime - aTime
} )
// both resolution for the duplicate QuickMythril card, and handling of all future duplicates that may be published...
if ( group [ 0 ] . identifier === 'QM-AR-card-Xw3dxL' ) {
console . warn ( ` This is a bug that allowed a duplicate prior to the logic displaying them based on original publisher only... displaying in reverse order... ` )
group [ 0 ] . isDuplicate = true
for ( let i = 1 ; i < group . length ; i ++ ) {
group [ i ] . isDuplicate = false
}
} else {
group [ 0 ] . isDuplicate = false
for ( let i = 1 ; i < group . length ; i ++ ) {
group [ i ] . isDuplicate = true
}
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-26 20:06:51 -08:00
}
2025-01-27 21:03:13 -08:00
// push them all
finalOutput . push ( ... group )
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-26 20:06:51 -08:00
}
2025-01-27 21:03:13 -08:00
// Sort final by newest overall
finalOutput . sort ( ( a , b ) => {
const aTime = a . updated || a . created || 0
const bTime = b . updated || b . created || 0
return bTime - aTime
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-26 20:06:51 -08:00
} )
2025-01-27 21:03:13 -08:00
return finalOutput
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-26 20:06:51 -08:00
}
2024-12-12 17:23:34 -08:00
//Main function to load the Minter Cards ----------------------------------------
2025-01-21 21:23:25 -08:00
const loadCards = async ( cardIdentifierPrefix ) => {
2024-12-30 21:39:18 -08:00
const cardsContainer = document . getElementById ( "cards-container" )
2025-01-21 21:23:25 -08:00
let isARBoard = false
2024-12-30 21:39:18 -08:00
cardsContainer . innerHTML = "<p>Loading cards...</p>"
2025-02-03 09:34:23 -08:00
const counterSpan = document . getElementById ( "board-card-counter" )
if ( counterSpan ) {
// Clear or show "Loading..."
counterSpan . textContent = "(loading...)"
}
2025-01-27 21:03:13 -08:00
if ( cardIdentifierPrefix . startsWith ( "QM-AR-card" ) ) {
2025-01-21 21:23:25 -08:00
isARBoard = true
console . warn ( ` ARBoard determined: ` , isARBoard )
}
2025-01-27 21:03:13 -08:00
let afterTime = 0
const timeRangeSelect = document . getElementById ( "time-range-select" )
2025-02-03 09:34:23 -08:00
const showExistingCheckbox = document . getElementById ( "show-existing-checkbox" )
const showExisting = showExistingCheckbox && showExistingCheckbox . checked
2025-01-27 21:03:13 -08:00
if ( timeRangeSelect ) {
const days = parseInt ( timeRangeSelect . value , 10 )
if ( days > 0 ) {
const now = Date . now ( )
const dayMs = 24 * 60 * 60 * 1000
afterTime = now - days * dayMs // e.g. last X days
console . log ( ` afterTime for last ${ days } days = ${ new Date ( afterTime ) . toLocaleString ( ) } ` )
}
}
2024-12-12 17:23:34 -08:00
try {
2025-01-27 21:03:13 -08:00
// 1) Fetch raw "BLOG_POST" entries
const response = await searchSimple ( 'BLOG_POST' , cardIdentifierPrefix , '' , 0 , 0 , '' , false , true , afterTime )
2024-12-12 17:23:34 -08:00
if ( ! response || ! Array . isArray ( response ) || response . length === 0 ) {
2024-12-30 21:39:18 -08:00
cardsContainer . innerHTML = "<p>No cards found.</p>"
2025-01-21 21:23:25 -08:00
return
2024-12-12 17:23:34 -08:00
}
2025-01-27 21:03:13 -08:00
// 2) Validate structure
2024-12-12 17:23:34 -08:00
const validatedCards = await Promise . all (
2025-01-27 21:03:13 -08:00
response . map ( async ( card ) => {
2024-12-30 21:39:18 -08:00
const isValid = await validateCardStructure ( card )
return isValid ? card : null
2024-12-12 17:23:34 -08:00
} )
2025-01-21 21:23:25 -08:00
)
2025-01-27 21:03:13 -08:00
const validCards = validatedCards . filter ( ( card ) => card !== null )
2024-12-12 17:23:34 -08:00
if ( validCards . length === 0 ) {
2024-12-30 21:39:18 -08:00
cardsContainer . innerHTML = "<p>No valid cards found.</p>"
return
2024-12-12 17:23:34 -08:00
}
2025-01-27 21:03:13 -08:00
// Additional logic for ARBoard or MinterCards
const finalCards = isARBoard
? await processARBoardCards ( validCards )
: await processMinterBoardCards ( validCards )
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-26 20:06:51 -08:00
2025-01-27 21:03:13 -08:00
// Sort finalCards according to selectedSort
2025-01-22 01:52:41 -05:00
let selectedSort = 'newest'
const sortSelect = document . getElementById ( 'sort-select' )
if ( sortSelect ) {
selectedSort = sortSelect . value
}
if ( selectedSort === 'name' ) {
finalCards . sort ( ( a , b ) => {
const nameA = a . name ? . toLowerCase ( ) || ''
const nameB = b . name ? . toLowerCase ( ) || ''
return nameA . localeCompare ( nameB )
} )
} else if ( selectedSort === 'recent-comments' ) {
2025-01-27 21:03:13 -08:00
// If you need the newest comment timestamp
2025-01-22 01:52:41 -05:00
for ( let card of finalCards ) {
card . newestCommentTimestamp = await getNewestCommentTimestamp ( card . identifier )
}
finalCards . sort ( ( a , b ) =>
( b . newestCommentTimestamp || 0 ) - ( a . newestCommentTimestamp || 0 )
)
} else if ( selectedSort === 'least-votes' ) {
2025-01-27 21:03:13 -08:00
await applyVoteSortingData ( finalCards , /* ascending= */ true )
2025-01-22 01:52:41 -05:00
} else if ( selectedSort === 'most-votes' ) {
2025-01-27 21:03:13 -08:00
await applyVoteSortingData ( finalCards , /* ascending= */ false )
2025-01-22 01:52:41 -05:00
}
2025-01-27 21:03:13 -08:00
// else 'newest' => do nothing (already sorted newest-first by your process functions).
// Create the 'finalCardsArray' that includes the data, etc.
let finalCardsArray = [ ]
2025-02-03 09:34:23 -08:00
let alreadyMinterCards = [ ]
2025-01-29 19:12:30 -08:00
cardsContainer . innerHTML = ''
2025-01-27 21:03:13 -08:00
for ( const card of finalCards ) {
2024-12-16 19:53:37 -08:00
try {
2025-01-29 19:12:30 -08:00
const skeletonHTML = createSkeletonCardHTML ( card . identifier )
cardsContainer . insertAdjacentHTML ( "beforeend" , skeletonHTML )
2024-12-16 19:53:37 -08:00
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : card . name ,
service : "BLOG_POST" ,
2025-01-27 21:03:13 -08:00
identifier : card . identifier
2024-12-30 21:39:18 -08:00
} )
2025-01-27 21:03:13 -08:00
if ( ! cardDataResponse || ! cardDataResponse . poll ) {
// skip
console . warn ( ` Skipping card: missing data/poll. identifier= ${ card . identifier } ` )
2025-01-29 19:12:30 -08:00
removeSkeleton ( card . identifier )
2025-01-27 21:03:13 -08:00
continue
}
// Extra validation: check poll ownership matches card publisher
2025-01-22 01:52:41 -05:00
const pollPublisherAddress = await getPollOwnerAddress ( cardDataResponse . poll )
const cardPublisherAddress = await fetchOwnerAddressFromName ( card . name )
2025-01-27 21:03:13 -08:00
if ( pollPublisherAddress !== cardPublisherAddress ) {
console . warn ( ` Poll hijack attack found, discarding card ${ card . identifier } ` )
2025-01-29 19:12:30 -08:00
removeSkeleton ( card . identifier )
2025-01-27 21:03:13 -08:00
continue
2024-12-30 21:39:18 -08:00
}
2025-01-27 21:03:13 -08:00
// If ARBoard, do a quick address check
2025-01-21 21:23:25 -08:00
if ( isARBoard ) {
2025-01-29 19:12:30 -08:00
const ok = await verifyMinter ( cardDataResponse . minterName )
2025-01-27 21:03:13 -08:00
if ( ! ok ) {
2025-01-29 19:12:30 -08:00
console . warn ( ` Card is not a minter nor an admin, not including in ARBoard. identifier: ${ card . identifier } ` )
removeSkeleton ( card . identifier )
continue
}
} else {
2025-01-29 20:18:30 -08:00
const isAlreadyMinter = await verifyMinter ( cardDataResponse . creator )
2025-01-29 19:12:30 -08:00
if ( isAlreadyMinter ) {
2025-02-03 09:34:23 -08:00
console . warn ( ` card IS ALREADY a minter, adding to alreadyMinterCards array: ${ card . identifier } ` )
2025-01-29 19:12:30 -08:00
removeSkeleton ( card . identifier )
2025-02-03 09:34:23 -08:00
alreadyMinterCards . push ( {
... card ,
cardDataResponse ,
pollPublisherAddress ,
cardPublisherAddress
} )
2025-01-27 21:03:13 -08:00
continue
2025-01-21 21:23:25 -08:00
}
}
2025-01-27 21:03:13 -08:00
// **Push** to finalCardsArray for further processing (duplicates, etc.)
finalCardsArray . push ( {
... card ,
cardDataResponse ,
pollPublisherAddress ,
cardPublisherAddress ,
} )
2025-02-03 09:34:23 -08:00
if ( counterSpan ) {
const displayedCount = finalCardsArray . length
const alreadyMinterCount = alreadyMinterCards . length
// If you want to show both
counterSpan . textContent = ` ( ${ displayedCount } cards, ${ alreadyMinterCount } existingMinters) `
}
2025-01-27 21:03:13 -08:00
} catch ( err ) {
console . error ( ` Error preparing card ${ card . identifier } ` , err )
2025-01-29 19:12:30 -08:00
removeSkeleton ( card . identifier )
2025-01-27 21:03:13 -08:00
}
}
2025-01-21 21:23:25 -08:00
2025-01-27 21:03:13 -08:00
// Next, do the actual rendering:
2025-01-29 19:12:30 -08:00
// cardsContainer.innerHTML = ""
2025-01-27 21:03:13 -08:00
for ( const cardObj of finalCardsArray ) {
// Insert a skeleton first if you like
2025-01-29 19:12:30 -08:00
// const skeletonHTML = createSkeletonCardHTML(cardObj.identifier)
// cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
2025-01-27 21:03:13 -08:00
// Build final HTML
const pollResults = await fetchPollResults ( cardObj . cardDataResponse . poll )
const commentCount = await countComments ( cardObj . identifier )
const cardUpdatedTime = cardObj . updated || null
const bgColor = generateDarkPastelBackgroundBy ( cardObj . name )
// Construct the final HTML for each card
const finalCardHTML = isARBoard
? await createARCardHTML (
cardObj . cardDataResponse ,
pollResults ,
cardObj . identifier ,
commentCount ,
cardUpdatedTime ,
bgColor ,
cardObj . cardPublisherAddress ,
cardObj . isDuplicate
)
: await createCardHTML (
cardObj . cardDataResponse ,
pollResults ,
cardObj . identifier ,
commentCount ,
cardUpdatedTime ,
bgColor ,
cardObj . cardPublisherAddress
)
replaceSkeleton ( cardObj . identifier , finalCardHTML )
}
2025-01-21 21:23:25 -08:00
2025-02-03 09:34:23 -08:00
if ( showExisting && alreadyMinterCards . length > 0 ) {
console . warn ( ` Rendering Existing Minter cards because user selected showExisting ` )
for ( const mintedCardObj of alreadyMinterCards ) {
const skeletonHTML = createSkeletonCardHTML ( mintedCardObj . identifier )
cardsContainer . insertAdjacentHTML ( "beforeend" , skeletonHTML )
const pollResults = await fetchPollResults ( mintedCardObj . cardDataResponse . poll )
const commentCount = await countComments ( mintedCardObj . identifier )
const cardUpdatedTime = mintedCardObj . updated || null
const bgColor = generateDarkPastelBackgroundBy ( mintedCardObj . name )
const isExistingMinter = true
const finalCardHTML = await createCardHTML (
mintedCardObj . cardDataResponse ,
pollResults ,
mintedCardObj . identifier ,
commentCount ,
cardUpdatedTime ,
bgColor ,
mintedCardObj . cardPublisherAddress ,
isExistingMinter
)
replaceSkeleton ( mintedCardObj . identifier , finalCardHTML )
}
}
2024-12-12 17:23:34 -08:00
} catch ( error ) {
2024-12-30 21:39:18 -08:00
console . error ( "Error loading cards:" , error )
cardsContainer . innerHTML = "<p>Failed to load cards.</p>"
2025-02-03 09:34:23 -08:00
if ( counterSpan ) {
counterSpan . textContent = "(error loading)"
}
2024-12-12 17:23:34 -08:00
}
2024-12-30 21:39:18 -08:00
}
2024-12-12 17:23:34 -08:00
2025-01-29 19:12:30 -08:00
const verifyMinter = async ( minterName ) => {
2025-01-27 21:03:13 -08:00
try {
const nameInfo = await getNameInfo ( minterName )
if ( ! nameInfo ) return false
const minterAddress = nameInfo . owner
const isValid = await getAddressInfo ( minterAddress )
if ( ! isValid ) return false
// Then check if they're in the minter group
const minterGroup = await fetchMinterGroupMembers ( )
const adminGroup = await fetchMinterGroupAdmins ( )
const minterGroupAddresses = minterGroup . map ( m => m . member )
const adminGroupAddresses = adminGroup . map ( m => m . member )
return ( minterGroupAddresses . includes ( minterAddress ) ||
adminGroupAddresses . includes ( minterAddress ) )
} catch ( err ) {
2025-01-29 19:12:30 -08:00
console . warn ( "verifyMinter error:" , err )
2025-01-27 21:03:13 -08:00
return false
}
}
const applyVoteSortingData = async ( cards , ascending = true ) => {
const minterGroupMembers = await fetchMinterGroupMembers ( )
const minterAdmins = await fetchMinterGroupAdmins ( )
for ( const card of cards ) {
try {
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : card . name ,
service : "BLOG_POST" ,
identifier : card . identifier ,
} )
if ( ! cardDataResponse || ! cardDataResponse . poll ) {
card . _adminVotes = 0
card . _adminYes = 0
card . _minterVotes = 0
card . _minterYes = 0
continue
}
const pollResults = await fetchPollResults ( cardDataResponse . poll ) ;
const { adminYes , adminNo , minterYes , minterNo } = await processPollData (
pollResults ,
minterGroupMembers ,
minterAdmins ,
cardDataResponse . creator ,
card . identifier
)
card . _adminVotes = adminYes + adminNo
card . _adminYes = adminYes
card . _minterVotes = minterYes + minterNo
card . _minterYes = minterYes
} catch ( error ) {
console . warn ( ` Error fetching or processing poll for card ${ card . identifier } : ` , error )
card . _adminVotes = 0
card . _adminYes = 0
card . _minterVotes = 0
card . _minterYes = 0
}
}
if ( ascending ) {
// least votes first
cards . sort ( ( a , b ) => {
const diffAdminTotal = a . _adminVotes - b . _adminVotes
if ( diffAdminTotal !== 0 ) return diffAdminTotal
const diffAdminYes = a . _adminYes - b . _adminYes
if ( diffAdminYes !== 0 ) return diffAdminYes
const diffMinterTotal = a . _minterVotes - b . _minterVotes
if ( diffMinterTotal !== 0 ) return diffMinterTotal
return a . _minterYes - b . _minterYes
} )
} else {
// most votes first
cards . sort ( ( a , b ) => {
const diffAdminTotal = b . _adminVotes - a . _adminVotes
if ( diffAdminTotal !== 0 ) return diffAdminTotal
const diffAdminYes = b . _adminYes - a . _adminYes
if ( diffAdminYes !== 0 ) return diffAdminYes
const diffMinterTotal = b . _minterVotes - a . _minterVotes
if ( diffMinterTotal !== 0 ) return diffMinterTotal
return b . _minterYes - a . _minterYes
} )
}
}
2024-12-16 19:53:37 -08:00
const removeSkeleton = ( cardIdentifier ) => {
2024-12-30 21:39:18 -08:00
const skeletonCard = document . getElementById ( ` skeleton- ${ cardIdentifier } ` )
2024-12-16 19:53:37 -08:00
if ( skeletonCard ) {
2024-12-30 21:39:18 -08:00
skeletonCard . remove ( )
2024-12-16 19:53:37 -08:00
}
2024-12-30 21:39:18 -08:00
}
2024-12-12 17:23:34 -08:00
2024-12-16 19:53:37 -08:00
const replaceSkeleton = ( cardIdentifier , htmlContent ) => {
2024-12-30 21:39:18 -08:00
const skeletonCard = document . getElementById ( ` skeleton- ${ cardIdentifier } ` )
2024-12-16 19:53:37 -08:00
if ( skeletonCard ) {
2024-12-30 21:39:18 -08:00
skeletonCard . outerHTML = htmlContent
2024-12-16 19:53:37 -08:00
}
2024-12-30 21:39:18 -08:00
}
2024-12-16 19:53:37 -08:00
const createSkeletonCardHTML = ( 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;" >
2024-12-20 22:07:18 -08:00
< div > < p style = "color:rgb(174, 174, 174)" > LOADING CARD ... < / p > < / d i v >
2024-12-16 19:53:37 -08:00
< 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;" >
2024-12-20 22:07:18 -08:00
< div style = "width: 100%; height: 80px; background-color: #eee; color:rgb(17, 24, 28); padding: 0.22vh" > < p > PLEASE BE PATIENT < / p > < p s t y l e = " c o l o r : # 1 1 1 2 1 c " > W h i l e d a t a l o a d s f r o m Q D N . . . < / d i v >
2024-12-16 19:53:37 -08:00
< / d i v >
< / d i v >
2024-12-30 21:39:18 -08:00
`
}
2024-12-14 19:40:31 -08:00
2024-12-12 17:23:34 -08:00
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
2025-01-21 21:23:25 -08:00
const fetchExistingCard = async ( cardIdentifierPrefix ) => {
2024-12-11 14:40:32 -08:00
try {
2025-01-01 10:47:37 -08:00
const response = await searchSimple ( 'BLOG_POST' , ` ${ cardIdentifierPrefix } ` , ` ${ userState . accountName } ` , 0 , 0 , '' , true )
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
console . log ( ` SEARCH_QDN_RESOURCES response: ${ JSON . stringify ( response , null , 2 ) } ` )
2024-12-11 14:40:32 -08:00
if ( ! response || ! Array . isArray ( response ) || response . length === 0 ) {
2024-12-28 22:49:18 -08:00
console . log ( "No cards found for the current user." )
return null
2024-12-27 23:04:16 -08:00
} else if ( response . length === 1 ) { // we don't need to go through all of the rest of the checks and filtering nonsense if there's only a single result, just return it.
2025-01-01 10:47:37 -08:00
const mostRecentCard = response [ 0 ]
2025-01-04 20:28:26 -08:00
isExistingCard = true
2025-01-01 10:47:37 -08:00
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : userState . accountName , // User's account name
service : "BLOG_POST" ,
identifier : mostRecentCard . identifier
} )
existingCardIdentifier = mostRecentCard . identifier
existingCardData = cardDataResponse
2025-01-04 20:28:26 -08:00
isExistingCard = true
2025-01-01 10:47:37 -08:00
return cardDataResponse
2024-12-11 14:40:32 -08:00
}
2024-12-12 17:23:34 -08:00
const validatedCards = await Promise . all (
response . map ( async card => {
2024-12-28 22:49:18 -08:00
const isValid = await validateCardStructure ( card )
return isValid ? card : null
2024-12-12 17:23:34 -08:00
} )
2024-12-28 22:49:18 -08:00
)
2024-12-12 17:23:34 -08:00
2024-12-28 22:49:18 -08:00
const validCards = validatedCards . filter ( card => card !== null )
2024-12-11 14:40:32 -08:00
if ( validCards . length > 0 ) {
2024-12-30 21:39:18 -08:00
2024-12-28 22:49:18 -08:00
const mostRecentCard = validCards . sort ( ( a , b ) => b . created - a . created ) [ 0 ]
2024-12-11 14:40:32 -08:00
2024-12-12 17:23:34 -08:00
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : userState . accountName , // User's account name
service : "BLOG_POST" ,
identifier : mostRecentCard . identifier
2024-12-28 22:49:18 -08:00
} )
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
existingCardIdentifier = mostRecentCard . identifier
existingCardData = cardDataResponse
2025-01-04 20:28:26 -08:00
isExistingCard = true
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
console . log ( "Full card data fetched successfully:" , cardDataResponse )
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
return cardDataResponse
2024-12-12 17:23:34 -08:00
}
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
console . log ( "No valid cards found." )
return null
2024-12-11 14:40:32 -08:00
} catch ( error ) {
2024-12-28 22:49:18 -08:00
console . error ( "Error fetching existing card:" , error )
return null
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
}
2024-12-12 17:23:34 -08:00
// Validate that a card is indeed a card and not a comment. -------------------------------------
const validateCardStructure = async ( card ) => {
2024-12-11 14:40:32 -08:00
return (
typeof card === "object" &&
card . name &&
card . service === "BLOG_POST" &&
card . identifier && ! card . identifier . includes ( "comment" ) &&
card . created
2024-12-28 22:49:18 -08:00
)
2024-12-11 14:40:32 -08:00
}
2024-12-12 17:23:34 -08:00
// Load existing card data passed, into the form for editing -------------------------------------
const loadCardIntoForm = async ( cardData ) => {
2024-12-28 22:49:18 -08:00
console . log ( "Loading existing card data:" , cardData )
document . getElementById ( "card-header" ) . value = cardData . header
document . getElementById ( "card-content" ) . value = cardData . content
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
const linksContainer = document . getElementById ( "links-container" )
linksContainer . innerHTML = ""
2024-12-11 14:40:32 -08:00
cardData . links . forEach ( link => {
2024-12-28 22:49:18 -08:00
const linkInput = document . createElement ( "input" )
linkInput . type = "text"
linkInput . className = "card-link"
2024-12-11 14:40:32 -08:00
linkInput . value = link ;
2025-01-01 13:47:45 -08:00
linksContainer . appendChild ( linkInput )
2024-12-28 22:49:18 -08:00
} )
2024-12-11 14:40:32 -08:00
}
2024-12-12 17:23:34 -08:00
// Main function to publish a new Minter Card -----------------------------------------------
2025-01-21 21:23:25 -08:00
const publishCard = async ( cardIdentifierPrefix ) => {
2024-12-28 22:49:18 -08:00
2024-12-30 21:39:18 -08:00
const minterGroupData = await fetchMinterGroupMembers ( )
const minterGroupAddresses = minterGroupData . map ( m => m . member )
2025-01-22 15:40:19 -08:00
const userAddress = userState . accountAddress
2024-12-28 22:49:18 -08:00
if ( minterGroupAddresses . includes ( userAddress ) ) {
2024-12-30 21:39:18 -08:00
alert ( "You are already a Minter and cannot publish a new card!" )
2025-01-01 13:47:45 -08:00
return
2024-12-28 22:49:18 -08:00
}
2024-12-30 21:39:18 -08:00
const header = document . getElementById ( "card-header" ) . value . trim ( )
const content = document . getElementById ( "card-content" ) . value . trim ( )
2024-12-11 14:40:32 -08:00
const links = Array . from ( document . querySelectorAll ( ".card-link" ) )
. map ( input => input . value . trim ( ) )
2024-12-28 22:49:18 -08:00
. filter ( link => link . startsWith ( "qortal://" ) )
2024-12-11 14:40:32 -08:00
if ( ! header || ! content ) {
2024-12-28 22:49:18 -08:00
alert ( "Header and content are required!" )
return
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
const cardIdentifier = isExistingCard ? existingCardIdentifier : ` ${ cardIdentifierPrefix } - ${ await uid ( ) } `
const pollName = ` ${ cardIdentifier } -poll `
const pollDescription = ` Mintership Board Poll for ${ userState . accountName } `
2024-12-11 14:40:32 -08:00
const cardData = {
header ,
content ,
links ,
creator : userState . accountName ,
2025-01-29 19:12:30 -08:00
creatorAddress : userState . accountAddress ,
2024-12-11 14:40:32 -08:00
timestamp : Date . now ( ) ,
poll : pollName ,
2024-12-28 22:49:18 -08:00
}
2024-12-12 17:23:34 -08:00
2024-12-11 14:40:32 -08:00
try {
2024-12-28 22:49:18 -08:00
let base64CardData = await objectToBase64 ( cardData )
2024-12-11 14:40:32 -08:00
if ( ! base64CardData ) {
2024-12-28 22:49:18 -08:00
console . log ( ` initial base64 object creation with objectToBase64 failed, using btoa... ` )
base64CardData = btoa ( JSON . stringify ( cardData ) )
2024-12-11 14:40:32 -08:00
}
await qortalRequest ( {
action : "PUBLISH_QDN_RESOURCE" ,
name : userState . accountName ,
service : "BLOG_POST" ,
identifier : cardIdentifier ,
data64 : base64CardData ,
2024-12-28 22:49:18 -08:00
} )
2024-12-11 16:41:20 -08:00
if ( ! isExistingCard ) {
await qortalRequest ( {
action : "CREATE_POLL" ,
pollName ,
pollDescription ,
pollOptions : [ 'Yes, No' ] ,
pollOwnerAddress : userState . accountAddress ,
2024-12-28 22:49:18 -08:00
} )
alert ( "Card and poll published successfully!" )
2024-12-11 16:41:20 -08:00
}
2024-12-28 22:49:18 -08:00
2024-12-11 16:41:20 -08:00
if ( isExistingCard ) {
2025-01-29 19:12:30 -08:00
alert ( "Card Updated Successfully! (No poll updates possible)" )
2025-01-04 20:28:26 -08:00
isExistingCard = false
2024-12-11 16:41:20 -08:00
}
2024-12-28 22:49:18 -08:00
document . getElementById ( "publish-card-form" ) . reset ( )
document . getElementById ( "publish-card-view" ) . style . display = "none"
document . getElementById ( "cards-container" ) . style . display = "flex"
2025-01-21 21:23:25 -08:00
await loadCards ( minterCardIdentifierPrefix )
2024-12-28 22:49:18 -08:00
2024-12-11 14:40:32 -08:00
} catch ( error ) {
2024-12-28 22:49:18 -08:00
console . error ( "Error publishing card or poll:" , error )
alert ( "Failed to publish card and poll." )
2024-12-11 14:40:32 -08:00
}
}
2025-01-01 13:47:45 -08:00
let globalVoterMap = new Map ( )
const processPollData = async ( pollData , minterGroupMembers , minterAdmins , creator , cardIdentifier ) => {
2024-12-28 22:49:18 -08:00
if ( ! pollData || ! Array . isArray ( pollData . voteWeights ) || ! Array . isArray ( pollData . votes ) ) {
console . warn ( "Poll data is missing or invalid. pollData:" , pollData )
return {
adminYes : 0 ,
adminNo : 0 ,
minterYes : 0 ,
minterNo : 0 ,
totalYes : 0 ,
totalNo : 0 ,
totalYesWeight : 0 ,
totalNoWeight : 0 ,
2025-01-22 01:52:41 -05:00
detailsHtml : ` <p>Poll data is invalid or missing.</p> ` ,
userVote : null
2024-12-28 22:49:18 -08:00
}
}
const memberAddresses = minterGroupMembers . map ( m => m . member )
const minterAdminAddresses = minterAdmins . map ( m => m . member )
2024-12-14 19:40:31 -08:00
const adminGroupsMembers = await fetchAllAdminGroupsMembers ( )
2025-01-13 15:53:20 -08:00
const featureTriggerPassed = await featureTriggerCheck ( )
2024-12-28 22:49:18 -08:00
const groupAdminAddresses = adminGroupsMembers . map ( m => m . member )
2025-01-13 15:53:20 -08:00
let adminAddresses = [ ... minterAdminAddresses ]
if ( ! featureTriggerPassed ) {
console . log ( ` featureTrigger is NOT passed, only showing admin results from Minter Admins and Group Admins ` )
adminAddresses = [ ... minterAdminAddresses , ... groupAdminAddresses ]
}
2024-12-28 22:49:18 -08:00
let adminYes = 0 , adminNo = 0
let minterYes = 0 , minterNo = 0
let yesWeight = 0 , noWeight = 0
2025-01-22 01:52:41 -05:00
let userVote = null
2024-12-28 22:49:18 -08:00
for ( const w of pollData . voteWeights ) {
if ( w . optionName . toLowerCase ( ) === 'yes' ) {
yesWeight = w . voteWeight
} else if ( w . optionName . toLowerCase ( ) === 'no' ) {
noWeight = w . voteWeight
}
}
2024-12-11 17:44:04 -08:00
2024-12-28 22:49:18 -08:00
const voterPromises = pollData . votes . map ( async ( vote ) => {
const optionIndex = vote . optionIndex ; // 0 => yes, 1 => no
const voterPublicKey = vote . voterPublicKey
const voterAddress = await getAddressFromPublicKey ( voterPublicKey )
2024-12-11 14:40:32 -08:00
2025-01-22 01:52:41 -05:00
if ( voterAddress === userState . accountAddress ) {
userVote = optionIndex
}
2024-12-28 22:49:18 -08:00
if ( optionIndex === 0 ) {
if ( adminAddresses . includes ( voterAddress ) ) {
adminYes ++
} else if ( memberAddresses . includes ( voterAddress ) ) {
minterYes ++
} else {
console . log ( ` voter ${ voterAddress } is not a minter nor an admin... Not included in aggregates. ` )
}
} else if ( optionIndex === 1 ) {
if ( adminAddresses . includes ( voterAddress ) ) {
adminNo ++
} else if ( memberAddresses . includes ( voterAddress ) ) {
minterNo ++
} else {
console . log ( ` voter ${ voterAddress } is not a minter nor an admin... Not included in aggregates. ` )
}
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
let voterName = ''
try {
const nameInfo = await getNameFromAddress ( voterAddress )
if ( nameInfo ) {
voterName = nameInfo
if ( nameInfo === voterAddress ) voterName = ''
}
} catch ( err ) {
console . warn ( ` No name for address ${ voterAddress } ` , err )
}
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
let blocksMinted = 0
try {
const addressInfo = await getAddressInfo ( voterAddress )
blocksMinted = addressInfo ? . blocksMinted || 0
} catch ( e ) {
console . warn ( ` Failed to get addressInfo for ${ voterAddress } ` , e )
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
const isAdmin = adminAddresses . includes ( voterAddress )
const isMinter = memberAddresses . includes ( voterAddress )
return {
optionIndex ,
voterPublicKey ,
voterAddress ,
voterName ,
isAdmin ,
isMinter ,
blocksMinted
}
} )
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
const allVoters = await Promise . all ( voterPromises )
const yesVoters = [ ]
const noVoters = [ ]
let totalMinterAndAdminYesWeight = 0
let totalMinterAndAdminNoWeight = 0
for ( const v of allVoters ) {
if ( v . optionIndex === 0 ) {
yesVoters . push ( v )
totalMinterAndAdminYesWeight += v . blocksMinted
} else if ( v . optionIndex === 1 ) {
noVoters . push ( v )
totalMinterAndAdminNoWeight += v . blocksMinted
}
}
2024-12-11 14:40:32 -08:00
2025-01-01 13:47:45 -08:00
yesVoters . sort ( ( a , b ) => b . blocksMinted - a . blocksMinted )
noVoters . sort ( ( a , b ) => b . blocksMinted - a . blocksMinted )
const sortedAllVoters = allVoters . sort ( ( a , b ) => b . blocksMinted - a . blocksMinted )
await createVoterMap ( sortedAllVoters , cardIdentifier )
2024-12-28 22:49:18 -08:00
const yesTableHtml = buildVotersTableHtml ( yesVoters , /* tableColor= */ "green" )
const noTableHtml = buildVotersTableHtml ( noVoters , /* tableColor= */ "red" )
const detailsHtml = `
< div class = "poll-details-container" id ' "${creator}-poll-details" >
< h1 style = "color:rgb(123, 123, 85); text-align: center; font-size: 2.0rem" > $ { creator } ' s < / h 1 > < h 3 s t y l e = " c o l o r : w h i t e ; t e x t - a l i g n : c e n t e r ; f o n t - s i z e : 1 . 8 r e m " > S u p p o r t P o l l R e s u l t D e t a i l s < / h 3 >
< h4 style = "color: green; text-align: center;" > Yes Vote Details < / h 4 >
$ { yesTableHtml }
< h4 style = "color: red; text-align: center; margin-top: 2em;" > No Vote Details < / h 4 >
$ { noTableHtml }
< / d i v >
`
2024-12-12 17:23:34 -08:00
const totalYes = adminYes + minterYes
const totalNo = adminNo + minterNo
2024-12-28 22:49:18 -08:00
return {
adminYes ,
adminNo ,
minterYes ,
minterNo ,
totalYes ,
totalNo ,
totalYesWeight : totalMinterAndAdminYesWeight ,
totalNoWeight : totalMinterAndAdminNoWeight ,
2025-01-22 01:52:41 -05:00
detailsHtml ,
userVote
2024-12-28 22:49:18 -08:00
}
}
2025-01-01 13:47:45 -08:00
const createVoterMap = async ( voters , cardIdentifier ) => {
const voterMap = new Map ( )
voters . forEach ( ( voter ) => {
const voterNameOrAddress = voter . voterName || voter . voterAddress
voterMap . set ( voterNameOrAddress , {
vote : voter . optionIndex === 0 ? "yes" : "no" , // Use optionIndex directly
voterType : voter . isAdmin ? "Admin" : voter . isMinter ? "Minter" : "User" ,
blocksMinted : voter . blocksMinted ,
} )
} )
globalVoterMap . set ( cardIdentifier , voterMap )
}
2024-12-28 22:49:18 -08:00
const buildVotersTableHtml = ( voters , tableColor ) => {
if ( ! voters . length ) {
2025-01-01 13:47:45 -08:00
return ` <p>No voters here.</p> `
2024-12-28 22:49:18 -08:00
}
// Decide extremely dark background for the <tbody>
2025-01-01 13:47:45 -08:00
let bodyBackground
2024-12-28 22:49:18 -08:00
if ( tableColor === "green" ) {
bodyBackground = "rgba(0, 18, 0, 0.8)" // near-black green
} else if ( tableColor === "red" ) {
bodyBackground = "rgba(30, 0, 0, 0.8)" // near-black red
} else {
// fallback color if needed
bodyBackground = "rgba(40, 20, 10, 0.8)"
}
// tableColor is used for the <thead>, bodyBackground for the <tbody>
const minterColor = 'rgb(98, 122, 167)'
const adminColor = 'rgb(44, 209, 151)'
const userColor = 'rgb(102, 102, 102)'
return `
< table style = "
width : 100 % ;
border - style : dotted ;
border - width : 0.15 rem ;
border - color : # 576 b6f ;
margin - bottom : 1 em ;
border - collapse : collapse ;
" >
< thead style = "background: ${tableColor}; color:rgb(238, 238, 238) ;" >
< tr style = "font-size: 1.5rem;" >
< th style = "padding: 0.1rem; text-align: center;" > Voter Name / Address < / t h >
< th style = "padding: 0.1rem; text-align: center;" > Voter Type < / t h >
< th style = "padding: 0.1rem; text-align: center;" > Voter Weight ( = BlocksMinted ) < / t h >
< / t r >
< / t h e a d >
<!-- Tbody with extremely dark green or red -- >
< tbody style = "background-color: ${bodyBackground}; color: #c6c6c6;" >
$ { voters
. map ( v => {
2025-01-01 13:47:45 -08:00
const userType = v . isAdmin ? "Admin" : v . isMinter ? "Minter" : "User"
2024-12-28 22:49:18 -08:00
const pollName = v . pollName
const displayName =
v . voterName
? v . voterName
: v . voterAddress
return `
< tr style = "font-size: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; font-weight: bold;" >
< td style = " padding : 1.2 rem ; border - width : 0.1 rem ; border - style : dotted ; border - color : lightgrey ; text - align : center ;
color : $ { userType === 'Admin' ? adminColor : v . isMinter ? minterColor : userColor } ; " > $ { displayName } < / t d >
< td style = " padding : 1.2 rem ; border - width : 0.1 rem ; border - style : dotted ; border - color : lightgrey ; text - align : center ;
color : $ { userType === 'Admin' ? adminColor : v . isMinter ? minterColor : userColor } ; " > $ { userType } < / t d >
< td style = " padding : 1.2 rem ; border - width : 0.1 rem ; border - style : dotted ; border - color : lightgrey ; text - align : center ;
color : $ { userType === 'Admin' ? adminColor : v . isMinter ? minterColor : userColor } ; " > $ { v . blocksMinted } < / t d >
< / t r >
`
} )
. join ( "" ) }
< / t b o d y >
< / t a b l e >
`
2024-12-12 17:23:34 -08:00
}
2024-12-11 14:40:32 -08:00
2024-12-28 22:49:18 -08:00
2025-01-29 19:12:30 -08:00
// Post a comment on a card. ---------------------------------
2024-12-11 14:40:32 -08:00
const postComment = async ( cardIdentifier ) => {
2024-12-28 22:49:18 -08:00
const commentInput = document . getElementById ( ` new-comment- ${ cardIdentifier } ` )
const commentText = commentInput . value . trim ( )
2024-12-11 14:40:32 -08:00
if ( ! commentText ) {
2024-12-28 22:49:18 -08:00
alert ( 'Comment cannot be empty!' )
return
2024-12-11 14:40:32 -08:00
}
try {
2025-01-29 19:12:30 -08:00
//Ensure the user is not on the blockList prior to allowing them to publish a comment.
const blockedNames = await fetchBlockList ( )
if ( blockedNames . includes ( userState . accountName ) ) {
alert ( 'You are on the block list and cannot publish comments.' )
return
}
const commentData = {
content : commentText ,
creator : userState . accountName ,
timestamp : Date . now ( ) ,
}
const uniqueCommentIdentifier = ` comment- ${ cardIdentifier } - ${ await uid ( ) } `
let base64CommentData = await objectToBase64 ( commentData )
2024-12-28 22:49:18 -08:00
if ( ! base64CommentData ) {
2025-01-29 19:12:30 -08:00
console . log ( 'objectToBase64 failed, fallback to btoa()' )
2024-12-28 22:49:18 -08:00
base64CommentData = btoa ( JSON . stringify ( commentData ) )
}
2025-01-29 19:12:30 -08:00
2024-12-11 14:40:32 -08:00
await qortalRequest ( {
action : 'PUBLISH_QDN_RESOURCE' ,
name : userState . accountName ,
service : 'BLOG_POST' ,
2025-01-29 19:12:30 -08:00
identifier : uniqueCommentIdentifier ,
2024-12-11 14:40:32 -08:00
data64 : base64CommentData ,
2024-12-28 22:49:18 -08:00
} )
2024-12-11 16:41:20 -08:00
2024-12-28 22:49:18 -08:00
commentInput . value = ''
2025-01-29 19:12:30 -08:00
2024-12-11 14:40:32 -08:00
} catch ( error ) {
2024-12-28 22:49:18 -08:00
console . error ( 'Error posting comment:' , error )
2025-01-29 19:12:30 -08:00
alert ( 'Failed to post comment. Error: ' + error )
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
}
2024-12-11 14:40:32 -08:00
2025-01-29 19:12:30 -08:00
2024-12-12 17:23:34 -08:00
//Fetch the comments for a card with passed card identifier ----------------------------
2024-12-11 14:40:32 -08:00
const fetchCommentsForCard = async ( cardIdentifier ) => {
try {
2024-12-28 22:49:18 -08:00
const response = await searchSimple ( 'BLOG_POST' , ` comment- ${ cardIdentifier } ` , '' , 0 , 0 , '' , 'false' )
2024-12-27 23:04:16 -08:00
return response
2024-12-11 14:40:32 -08:00
} catch ( error ) {
2024-12-27 23:04:16 -08:00
console . error ( ` Error fetching comments for ${ cardIdentifier } : ` , error )
return [ ]
2024-12-11 14:40:32 -08:00
}
2024-12-27 23:04:16 -08:00
}
2024-12-11 14:40:32 -08:00
const displayComments = async ( cardIdentifier ) => {
try {
2025-01-01 13:47:45 -08:00
const comments = await fetchCommentsForCard ( cardIdentifier )
2024-12-28 22:49:18 -08:00
const commentsContainer = document . getElementById ( ` comments-container- ${ cardIdentifier } ` )
2025-01-29 19:12:30 -08:00
commentsContainer . innerHTML = ""
const blockedNames = await fetchBlockList ( )
console . log ( "Loaded block list:" , blockedNames )
2025-01-01 13:47:45 -08:00
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 : "BLOG_POST" ,
2025-01-29 19:12:30 -08:00
identifier : comment . identifier
2025-01-01 13:47:45 -08:00
} )
2025-01-29 19:12:30 -08:00
if ( ! commentDataResponse || ! commentDataResponse . creator ) {
return null
}
const commenterName = commentDataResponse . creator
const voterInfo = voterMap . get ( commenterName )
2025-01-01 13:47:45 -08:00
let commentColor = "transparent"
let adminBadge = ""
2025-01-29 19:12:30 -08:00
if ( blockedNames . includes ( commenterName ) ) {
console . warn ( ` Skipping blocked commenter: ${ commenterName } ` )
return null
}
2025-01-01 13:47:45 -08:00
if ( voterInfo ) {
if ( voterInfo . voterType === "Admin" ) {
commentColor = voterInfo . vote === "yes" ? "rgba(21, 150, 21, 0.6)" : "rgba(212, 37, 64, 0.6)" // Light green for yes, light red for no
2025-01-29 19:12:30 -08:00
const badgeColor = voterInfo . vote === "yes" ? "rgb(206, 195, 77)" : "rgb(121, 119, 90)"
2025-01-01 13:47:45 -08:00
adminBadge = ` <span style="color: ${ badgeColor } ; font-weight: bold; margin-left: 0.5em;">(Admin)</span> `
} else {
commentColor = voterInfo . vote === "yes" ? "rgba(0, 100, 0, 0.3)" : "rgba(100, 0, 0, 0.3)" // Darker green for yes, darker red for no
}
}
2025-01-29 19:12:30 -08:00
const timestamp = new Date ( commentDataResponse . timestamp ) . toLocaleString ( )
2025-01-01 13:47:45 -08:00
return `
< div class = "comment" style = "border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: ${commentColor};" >
< p >
2025-01-29 19:12:30 -08:00
< strong > $ { commenterName } < / s t r o n g >
2025-01-01 13:47:45 -08:00
$ { adminBadge }
< / p >
< p > $ { commentDataResponse . content } < / p >
< p > < i > $ { timestamp } < / i > < / p >
< / d i v >
`
} catch ( err ) {
2025-01-29 19:12:30 -08:00
console . error ( ` Error with comment ${ comment . identifier } : ` , err )
2025-01-01 13:47:45 -08:00
return null
}
2024-12-28 22:49:18 -08:00
} )
2025-01-01 13:47:45 -08:00
)
commentHTMLArray
2025-01-29 19:12:30 -08:00
. filter ( html => html !== null )
. forEach ( commentHTML => {
2025-01-01 13:47:45 -08:00
commentsContainer . insertAdjacentHTML ( 'beforeend' , commentHTML )
} )
2025-01-29 19:12:30 -08:00
} catch ( err ) {
console . error ( ` Error displaying comments for ${ cardIdentifier } : ` , err )
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
}
2024-12-11 14:40:32 -08:00
2025-01-29 19:12:30 -08:00
2024-12-12 17:23:34 -08:00
// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
2024-12-11 14:40:32 -08:00
const toggleComments = async ( cardIdentifier ) => {
2024-12-28 22:49:18 -08:00
const commentsSection = document . getElementById ( ` comments-section- ${ 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-26 20:06:51 -08:00
const commentButton = document . getElementById ( ` comment-button- ${ cardIdentifier } ` )
2024-12-28 22:49:18 -08:00
if ( ! commentsSection || ! commentButton ) return
2025-02-03 09:34:23 -08:00
2024-12-28 22:49:18 -08:00
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-26 20:06:51 -08:00
if ( isHidden ) {
// Show comments
2024-12-28 22:49:18 -08:00
commentButton . textContent = "LOADING..."
2024-12-30 21:39:18 -08:00
await displayComments ( cardIdentifier )
2024-12-28 22:49:18 -08:00
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-26 20:06:51 -08:00
// Change the button text to 'HIDE COMMENTS'
2024-12-28 22:49:18 -08:00
commentButton . textContent = 'HIDE COMMENTS'
2024-12-11 14:40:32 -08: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-26 20:06:51 -08:00
// Hide comments
2024-12-28 22:49:18 -08:00
commentsSection . style . display = 'none'
commentButton . textContent = ` COMMENTS ( ${ count } ) `
2024-12-11 14:40:32 -08:00
}
2024-12-28 22:49:18 -08:00
}
2024-12-11 14:40:32 -08:00
2024-12-20 22:07:18 -08:00
const countComments = async ( cardIdentifier ) => {
try {
2024-12-28 22:49:18 -08:00
const response = await searchSimple ( 'BLOG_POST' , ` comment- ${ cardIdentifier } ` , '' , 0 , 0 , '' , 'false' )
return Array . isArray ( response ) ? response . length : 0
2024-12-20 22:07:18 -08:00
} catch ( error ) {
2024-12-28 22:49:18 -08:00
console . error ( ` Error fetching comment count for ${ cardIdentifier } : ` , error )
return 0
2024-12-20 22:07:18 -08:00
}
2024-12-28 22:49:18 -08:00
}
2025-01-04 20:28:26 -08:00
2024-12-28 22:49:18 -08:00
const createModal = ( modalType = '' ) => {
if ( document . getElementById ( ` ${ modalType } -modal ` ) ) {
return
}
const isIframe = ( modalType === 'links' )
2024-12-20 22:07:18 -08:00
2024-12-12 17:23:34 -08:00
const modalHTML = `
2024-12-28 22:49:18 -08:00
< div id = "${modalType}-modal"
style = " display : none ;
position : fixed ;
top : 0 ; left : 0 ;
width : 100 % ; height : 100 % ;
background : rgba ( 0 , 0 , 0 , 0.50 ) ;
z - index : 10000 ; " >
< div id = "${modalType}-modalContainer"
style = " position : relative ;
margin : 10 % auto ;
width : 80 % ;
height : 70 % ;
background : rgba ( 0 , 0 , 0 , 0.80 ) ;
border - radius : 10 px ;
overflow : hidden ; " >
$ {
isIframe
? ` <iframe id=" ${ modalType } -modalContent"
src = ""
style = "width: 100%; height: 100%; border: none;" >
< / i f r a m e > `
: ` <div id=" ${ modalType } -modalContent"
style = "width: 100%; height: 100%; overflow: auto;" >
< / d i v > `
}
< button onclick = "closeModal('${modalType}')"
style = " position : absolute ; top : 0.2 rem ; right : 0.2 rem ;
background : rgba ( 0 , 0 , 0 , 0.66 ) ; color : white ; border : none ;
font - size : 2.2 rem ;
padding : 0.4 rem 1 rem ;
border - radius : 0.33 rem ;
border - style : dashed ;
border - color : rgb ( 213 , 224 , 225 ) ;
"
onmouseover = "this.style.backgroundColor='rgb(73, 7, 7) '"
onmouseout = "this.style.backgroundColor='rgba(5, 14, 11, 0.63) '" >
X
< / b u t t o n >
2024-12-12 17:23:34 -08:00
< / d i v >
< / d i v >
2024-12-28 22:49:18 -08:00
`
document . body . insertAdjacentHTML ( 'beforeend' , modalHTML )
const modal = document . getElementById ( ` ${ modalType } -modal ` )
2025-01-29 19:12:30 -08:00
2024-12-28 22:49:18 -08:00
window . addEventListener ( 'click' , ( event ) => {
if ( event . target === modal ) {
closeModal ( modalType )
}
} )
2024-12-12 17:23:34 -08:00
}
2024-12-28 22:49:18 -08:00
const openLinksModal = async ( link ) => {
const processedLink = await processLink ( link )
const modal = document . getElementById ( 'links-modal' )
const modalContent = document . getElementById ( 'links-modalContent' )
modalContent . src = processedLink
modal . style . display = 'block'
2024-12-12 17:23:34 -08:00
}
2024-12-28 22:49:18 -08:00
const closeModal = async ( modalType = 'links' ) => {
const modal = document . getElementById ( ` ${ modalType } -modal ` )
const modalContent = document . getElementById ( ` ${ modalType } -modalContent ` )
if ( modal ) {
modal . style . display = 'none'
}
if ( modalContent ) {
modalContent . src = ''
}
2024-12-12 17:23:34 -08:00
}
const processLink = async ( link ) => {
if ( link . startsWith ( 'qortal://' ) ) {
2024-12-28 22:49:18 -08:00
const match = link . match ( /^qortal:\/\/([^/]+)(\/.*)?$/ )
2024-12-12 21:49:14 -08:00
if ( match ) {
2024-12-28 22:49:18 -08:00
const firstParam = match [ 1 ] . toUpperCase ( )
const remainingPath = match [ 2 ] || ""
const themeColor = window . _qdnTheme || 'default'
await new Promise ( resolve => setTimeout ( resolve , 10 ) )
return ` /render/ ${ firstParam } ${ remainingPath } ?theme= ${ themeColor } `
2024-12-12 21:49:14 -08:00
}
2024-12-12 17:23:34 -08:00
}
2024-12-28 22:49:18 -08:00
return link
}
const togglePollDetails = ( cardIdentifier ) => {
const detailsDiv = document . getElementById ( ` poll-details- ${ cardIdentifier } ` )
const modal = document . getElementById ( ` poll-details-modal ` )
const modalContent = document . getElementById ( ` poll-details-modalContent ` )
if ( ! detailsDiv || ! modal || ! modalContent ) return
modalContent . innerHTML = detailsDiv . innerHTML
modal . style . display = 'block'
2025-01-29 19:12:30 -08:00
2024-12-28 22:49:18 -08:00
window . onclick = ( event ) => {
if ( event . target === modal ) {
modal . style . display = 'none'
}
}
}
2024-12-20 22:07:18 -08:00
const generateDarkPastelBackgroundBy = ( name ) => {
2024-12-28 22:49:18 -08:00
let hash = 0
2024-12-20 22:07:18 -08:00
for ( let i = 0 ; i < name . length ; i ++ ) {
2024-12-28 22:49:18 -08:00
hash = ( hash << 5 ) - hash + name . charCodeAt ( i )
hash |= 0
2024-12-20 22:07:18 -08:00
}
2024-12-28 22:49:18 -08:00
const safeHash = Math . abs ( hash )
const hueSteps = 69.69
const hueIndex = safeHash % hueSteps
const hueRange = 288
const hue = 140 + ( hueIndex * ( hueRange / hueSteps ) )
2024-12-20 22:07:18 -08:00
2024-12-28 22:49:18 -08:00
const satSteps = 13.69
const satIndex = safeHash % satSteps
const saturation = 18 + ( satIndex * 1.333 )
2024-12-20 22:07:18 -08:00
2024-12-28 22:49:18 -08:00
const lightSteps = 3.69
const lightIndex = safeHash % lightSteps
const lightness = 7 + lightIndex
2024-12-12 17:23:34 -08:00
2024-12-28 22:49:18 -08:00
return ` hsl( ${ hue } , ${ saturation } %, ${ lightness } %) `
}
2024-12-12 17:23:34 -08:00
2025-01-04 20:28:26 -08:00
const handleInviteMinter = async ( minterName ) => {
try {
const blockInfo = await getLatestBlockInfo ( )
2025-01-06 18:43:54 -08:00
const blockHeight = blockInfo . height
2025-01-04 20:28:26 -08:00
const minterAccountInfo = await getNameInfo ( minterName )
const minterAddress = await minterAccountInfo . owner
2025-01-08 20:26:24 -08:00
let adminPublicKey
let txGroupId
if ( blockHeight >= GROUP _APPROVAL _FEATURE _TRIGGER _HEIGHT ) {
if ( userState . isMinterAdmin ) {
adminPublicKey = await getPublicKeyByName ( userState . accountName )
txGroupId = 694
} else {
console . warn ( ` user is not a minter admin, cannot create invite! ` )
return
}
} else {
adminPublicKey = await getPublicKeyByName ( userState . accountName )
txGroupId = 0
}
const fee = 0.01
const timeToLive = 864000
2025-01-04 20:28:26 -08:00
2025-01-08 20:26:24 -08:00
console . log ( ` about to attempt group invite, minterAddress: ${ minterAddress } , adminPublicKey: ${ adminPublicKey } ` )
const inviteTransaction = await createGroupInviteTransaction ( minterAddress , adminPublicKey , 694 , minterAddress , timeToLive , txGroupId , fee )
2025-01-04 20:28:26 -08:00
const signedTransaction = await qortalRequest ( {
action : "SIGN_TRANSACTION" ,
unsignedBytes : inviteTransaction
} )
console . warn ( ` signed transaction ` , signedTransaction )
const processResponse = await processTransaction ( signedTransaction )
2025-01-08 20:26:24 -08:00
if ( typeof processResponse === 'object' ) {
// The successful object might have a "signature" or "type" or "approvalStatus"
console . log ( "Invite transaction success object:" , processResponse )
alert ( ` ${ minterName } has been successfully invited! Wait for confirmation...Transaction Response: ${ JSON . stringify ( processResponse ) } ` )
2025-01-04 20:28:26 -08:00
} else {
2025-01-08 20:26:24 -08:00
// fallback string or something
console . log ( "Invite transaction raw text response:" , processResponse )
alert ( ` Invite transaction response: ${ JSON . stringify ( processResponse ) } ` )
2025-01-04 20:28:26 -08:00
}
2025-01-08 20:26:24 -08:00
2025-01-04 20:28:26 -08:00
} catch ( error ) {
console . error ( "Error inviting minter:" , error )
alert ( "Error inviting minter. Please try again." )
}
}
2025-01-29 19:12:30 -08:00
const escapeHTML = ( str ) => {
return str
. replace ( /'/g , ''' )
. replace ( /"/g , '"' )
}
2025-01-04 20:28:26 -08:00
const createInviteButtonHtml = ( creator , cardIdentifier ) => {
2025-01-29 19:12:30 -08:00
const escapedCreator = escapeHTML ( creator )
2025-01-04 20:28:26 -08:00
return `
< div id = "invite-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
2025-01-29 19:12:30 -08:00
< button onclick = "handleInviteMinter('${escapedCreator}')"
2025-01-13 15:53:20 -08:00
style = "padding: 10px; background:rgb(0, 109, 76) ; color: white; border: dotted; border-color: white; cursor: pointer; border-radius: 5px;"
2025-01-04 20:28:26 -08:00
onmouseover = "this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout = "this.style.backgroundColor='rgba(7, 122, 101, 0.63) '"
>
2025-01-13 15:53:20 -08:00
Create Minter Invite
2025-01-04 20:28:26 -08:00
< / b u t t o n >
< / d i v >
`
}
2025-01-13 15:53:20 -08:00
const featureTriggerCheck = async ( ) => {
2025-01-04 20:28:26 -08:00
const latestBlockInfo = await getLatestBlockInfo ( )
2025-01-06 18:43:54 -08:00
const isBlockPassed = latestBlockInfo . height >= GROUP _APPROVAL _FEATURE _TRIGGER _HEIGHT
2025-01-13 15:53:20 -08:00
if ( isBlockPassed ) {
console . warn ( ` featureTrigger check (verifyFeatureTrigger) determined block has PASSED: ` , isBlockPassed )
featureTriggerPassed = true
return true
} else {
console . warn ( ` featureTrigger check (verifyFeatureTrigger) determined block has NOT PASSED: ` , isBlockPassed )
featureTriggerPassed = false
return false
}
}
const checkAndDisplayInviteButton = async ( adminYes , creator , cardIdentifier ) => {
2025-01-29 19:12:30 -08:00
const isSomeTypaAdmin = userState . isAdmin || userState . isMinterAdmin
2025-01-13 15:53:20 -08:00
const isBlockPassed = await featureTriggerCheck ( )
2025-01-29 19:12:30 -08:00
const minterAdmins = await fetchMinterGroupAdmins ( )
2025-01-08 20:26:24 -08:00
2025-01-29 19:12:30 -08:00
// default needed admin count = 9, or 40% if block has passed
2025-01-21 21:23:25 -08:00
let minAdminCount = 9
if ( isBlockPassed ) {
2025-01-29 19:12:30 -08:00
minAdminCount = Math . ceil ( minterAdmins . length * 0.4 )
2025-01-21 21:23:25 -08:00
console . warn ( ` Using 40% => ${ minAdminCount } ` )
2025-01-08 20:26:24 -08:00
}
2025-01-29 19:12:30 -08:00
// if not enough adminYes votes, no invite button
2025-01-21 21:23:25 -08:00
if ( adminYes < minAdminCount ) {
console . warn ( ` Admin votes not high enough (have= ${ adminYes } , need= ${ minAdminCount } ). No button. ` )
return null
2025-01-08 20:26:24 -08:00
}
2025-01-29 19:12:30 -08:00
console . log ( ` passed initial button creation checks (adminYes >= ${ minAdminCount } ) ` )
// get user's address from 'creator' name
2025-01-21 21:23:25 -08:00
const minterNameInfo = await getNameInfo ( creator )
2025-01-29 19:12:30 -08:00
if ( ! minterNameInfo || ! minterNameInfo . owner ) {
console . warn ( ` No valid nameInfo for ${ creator } , skipping invite button. ` )
return null
}
const minterAddress = minterNameInfo . owner
// fetch all final KICK/BAN tx
const { finalKickTxs , finalBanTxs } = await fetchAllKickBanTxData ( )
2025-01-31 16:37:16 -08:00
const { finalInviteTxs , pendingInviteTxs } = await fetchAllInviteTransactions ( )
2025-02-05 20:01:01 -08:00
// check if there's a KICK or BAN for this user.
2025-01-29 19:12:30 -08:00
const priorKick = finalKickTxs . some ( tx => tx . member === minterAddress )
const priorBan = finalBanTxs . some ( tx => tx . offender === minterAddress )
2025-01-31 16:37:16 -08:00
const existingInvite = finalInviteTxs . some ( tx => tx . invitee === minterAddress )
const pendingInvite = pendingInviteTxs . some ( tx => tx . invitee === minterAddress )
2025-01-29 19:12:30 -08:00
const priorBanOrKick = ( priorBan || priorKick )
console . warn ( ` PriorBanOrKick determination for ${ minterAddress } : ` , priorBanOrKick )
// build the normal invite button & groupApprovalHtml
2025-01-31 16:37:16 -08:00
let inviteButtonHtml = ""
if ( existingInvite || pendingInvite ) {
2025-02-05 20:01:01 -08:00
console . warn ( ` There is an EXISTING or PENDING INVITE for this user! No invite button being created... existing: ( ${ existingInvite } , pending: ${ pendingInvite } ) ` )
2025-01-31 16:37:16 -08:00
inviteButtonHtml = ''
2025-02-05 20:01:01 -08:00
} else {
inviteButtonHtml = isSomeTypaAdmin ? createInviteButtonHtml ( creator , cardIdentifier ) : ""
2025-01-31 16:37:16 -08:00
}
2025-02-05 20:01:01 -08:00
2025-01-21 21:23:25 -08:00
const groupApprovalHtml = await checkGroupApprovalAndCreateButton ( minterAddress , cardIdentifier , "GROUP_INVITE" )
2025-01-04 20:28:26 -08:00
2025-01-29 19:12:30 -08:00
// if user had no prior KICK/BAN
2025-01-21 21:23:25 -08:00
if ( ! priorBanOrKick ) {
2025-01-29 19:12:30 -08:00
console . log ( ` No prior kick/ban found, creating invite (or approve) button... ` )
2025-01-21 21:23:25 -08:00
console . warn ( ` Existing Numbers - adminYes/minAdminCount: ${ adminYes } / ${ minAdminCount } ` )
2025-01-29 19:12:30 -08:00
// if there's already a pending GROUP_INVITE, return that approval button
if ( groupApprovalHtml ) {
2025-01-21 21:23:25 -08:00
console . warn ( ` groupApprovalCheck found existing groupApproval, returning approval button instead of invite button... ` )
return groupApprovalHtml
}
2025-01-29 19:12:30 -08:00
console . warn ( ` No pending approvals or prior kick/ban found, returning invite button... ` )
2025-01-04 20:28:26 -08:00
return inviteButtonHtml
2025-01-29 19:12:30 -08:00
} else {
// priorBanOrKick is true => show both
console . warn ( ` Prior kick/ban found! Including BOTH buttons... ` )
2025-01-21 21:23:25 -08:00
return inviteButtonHtml + groupApprovalHtml
}
2025-01-04 20:28:26 -08:00
}
2025-01-13 15:53:20 -08:00
const findPendingApprovalTxForAddress = async ( address , txType , limit = 0 , offset = 0 ) => {
// 1) Fetch all pending transactions
2025-01-31 16:37:16 -08:00
const pendingTxs = await searchPendingTransactions ( limit , offset , false )
2025-01-13 15:53:20 -08:00
// if a txType is passed, return the results related to that type, if not, then return any pending tx of the potential types.
let relevantTypes
if ( txType ) {
relevantTypes = new Set ( [ txType ] )
} else {
2025-01-21 21:23:25 -08:00
relevantTypes = new Set ( [ "GROUP_INVITE" , "GROUP_BAN" , "GROUP_KICK" , "ADD_GROUP_ADMIN" , "REMOVE_GROUP_ADMIN" ] )
2025-01-13 15:53:20 -08:00
}
// Filter pending TX for relevant types
const relevantTxs = pendingTxs . filter ( ( tx ) => relevantTypes . has ( tx . type ) )
const matchedTxs = relevantTxs . filter ( ( tx ) => {
switch ( tx . type ) {
case "GROUP_INVITE" :
return tx . invitee === address
case "GROUP_BAN" :
return tx . offender === address
case "GROUP_KICK" :
return tx . member === address
2025-01-21 21:23:25 -08:00
case "ADD_GROUP_ADMIN" :
return tx . member === address
case "REMOVE_GROUP_ADMIN" :
return tx . admin === address
2025-01-13 15:53:20 -08:00
default :
return false
}
} )
2025-01-21 21:23:25 -08:00
console . warn ( ` matchedTxs: ` , matchedTxs )
2025-01-31 16:37:16 -08:00
//Sort oldest→newest by timestamp, so matchedTxs[0] is the oldest
matchedTxs . sort ( ( a , b ) => a . timestamp - b . timestamp )
2025-01-13 15:53:20 -08:00
return matchedTxs // Array of matching pending transactions
}
2025-01-08 20:26:24 -08:00
const checkGroupApprovalAndCreateButton = async ( address , cardIdentifier , transactionType ) => {
2025-01-31 16:37:16 -08:00
// We are going to be verifying that the address isn't already a minter, before showing GROUP_APPROVAL buttons potentially...
if ( transactionType === "GROUP_INVITE" ) {
console . log ( ` This is a GROUP_INVITE check for group approval... Checking that user isn't already a minter... ` )
const minterMembers = await fetchMinterGroupMembers ( )
const minterGroupAddresses = minterMembers . map ( m => m . member )
if ( minterGroupAddresses . includes ( address ) ) {
console . warn ( ` User is already a minter, will not be creating group_approval buttons ` )
return null
}
}
2025-01-13 15:53:20 -08:00
const approvalSearchResults = await searchTransactions ( {
2025-01-21 21:23:25 -08:00
txTypes : [ 'GROUP_APPROVAL' ] ,
2025-01-13 15:53:20 -08:00
confirmationStatus : 'CONFIRMED' ,
limit : 0 ,
2025-01-31 16:37:16 -08:00
reverse : false ,
2025-01-13 15:53:20 -08:00
offset : 0 ,
startBlock : 1990000 ,
blockLimit : 0 ,
txGroupId : 0
} )
2025-01-31 16:37:16 -08:00
const pendingApprovals = await findPendingApprovalTxForAddress ( address , transactionType , 0 , 0 )
2025-01-29 19:12:30 -08:00
let isSomeTypaAdmin = userState . isAdmin || userState . isMinterAdmin
2025-01-21 21:23:25 -08:00
// If no pending transaction found, return null
if ( ! pendingApprovals || pendingApprovals . length === 0 ) {
console . warn ( "no pending approval transactions found, returning null..." )
2025-01-29 19:12:30 -08:00
return null
2025-01-08 20:26:24 -08:00
}
const txSig = pendingApprovals [ 0 ] . signature
2025-01-31 16:37:16 -08:00
// Find the relevant signature. (First approval)
2025-01-21 21:23:25 -08:00
const relevantApprovals = approvalSearchResults . filter (
( approvalTx ) => approvalTx . pendingSignature === txSig
)
const { tableHtml , uniqueApprovalCount } = await buildApprovalTableHtml (
relevantApprovals ,
getNameFromAddress
)
2025-01-08 20:26:24 -08:00
2025-01-29 19:12:30 -08:00
if ( transactionType === "GROUP_INVITE" && isSomeTypaAdmin ) {
2025-01-08 20:26:24 -08:00
const approvalButtonHtml = `
2025-01-21 21:23:25 -08:00
< div style = "display: flex; flex-direction: column; margin-top: 1em;" >
< p style = "color: rgb(181, 214, 100);" >
Existing $ { transactionType } Approvals : $ { uniqueApprovalCount }
< / p >
$ { tableHtml }
< div id = "approval-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button
style = "
padding : 8 px ;
background : rgb ( 37 , 97 , 99 ) ;
color : rgb ( 215 , 215 , 215 ) ;
border : 1 px solid # 333 ;
border - color : white ;
border - radius : 5 px ;
cursor : pointer ;
"
onmouseover = "this.style.backgroundColor='rgb(25, 47, 39)'"
onmouseout = "this.style.backgroundColor='rgb(37, 96, 99)'"
onclick = "handleGroupApproval('${txSig}')"
>
Approve Invite Tx
< / b u t t o n >
< / d i v >
< / d i v >
2025-01-08 20:26:24 -08:00
`
return approvalButtonHtml
}
2025-01-21 21:23:25 -08:00
2025-01-29 19:12:30 -08:00
if ( transactionType === "GROUP_KICK" && isSomeTypaAdmin ) {
2025-01-08 20:26:24 -08:00
const approvalButtonHtml = `
2025-01-21 21:23:25 -08:00
< div style = "display: flex; flex-direction: column; margin-top: 1em;" >
< p style = "color: rgb(199, 100, 64);" >
Existing $ { transactionType } Approvals : $ { uniqueApprovalCount }
< / p >
$ { tableHtml }
< div id = "approval-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button
style = "
padding : 8 px ;
background : rgb ( 119 , 91 , 21 ) ;
color : rgb ( 201 , 255 , 251 ) ;
border : 1 px solid # 333 ;
border - color : rgb ( 102 , 69 , 60 ) ;
border - radius : 5 px ;
cursor : pointer ;
"
onmouseover = "this.style.backgroundColor='rgb(50, 52, 51)'"
onmouseout = "this.style.backgroundColor='rgb(119, 91, 21)'"
onclick = "handleGroupApproval('${txSig}')"
>
Approve Kick Tx
< / b u t t o n >
< / d i v >
< / d i v >
2025-01-08 20:26:24 -08:00
`
return approvalButtonHtml
}
2025-01-21 21:23:25 -08:00
2025-01-29 19:12:30 -08:00
if ( transactionType === "GROUP_BAN" && isSomeTypaAdmin ) {
2025-01-08 20:26:24 -08:00
const approvalButtonHtml = `
2025-01-21 21:23:25 -08:00
< div style = "display: flex; flex-direction: column; margin-top: 1em;" >
< p style = "color: rgb(189, 40, 40);" >
Existing $ { transactionType } Approvals : $ { uniqueApprovalCount }
< / p >
$ { tableHtml }
< div id = "approval-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button
style = "
padding : 8 px ;
background : rgb ( 54 , 7 , 7 ) ;
color : rgb ( 201 , 255 , 251 ) ;
border : 1 px solid # 333 ;
border - color : rgb ( 204 , 94 , 94 ) ;
border - radius : 5 px ;
cursor : pointer ;
"
onmouseover = "this.style.backgroundColor='rgb(50, 52, 51)'"
onmouseout = "this.style.backgroundColor='rgb(54, 7, 7)'"
onclick = "handleGroupApproval('${txSig}')"
>
Approve Ban Tx
< / b u t t o n >
< / d i v >
< / d i v >
`
return approvalButtonHtml
}
2025-01-29 19:12:30 -08:00
if ( transactionType === "ADD_GROUP_ADMIN" && isSomeTypaAdmin ) {
2025-01-21 21:23:25 -08:00
const approvalButtonHtml = `
< div style = "display: flex; flex-direction: column; margin-top: 1em;" >
< p style = "color: rgb(40, 144, 189);" >
Existing $ { transactionType } Approvals : $ { uniqueApprovalCount }
< / p >
$ { tableHtml }
< div id = "approval-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button
style = "
padding : 8 px ;
background : rgb ( 8 , 71 , 69 ) ;
color : rgb ( 201 , 255 , 251 ) ;
border : 1 px solid # 333 ;
border - color : rgb ( 198 , 252 , 249 ) ;
border - radius : 5 px ;
cursor : pointer ;
"
onmouseover = "this.style.backgroundColor='rgb(17, 41, 29)'"
onmouseout = "this.style.backgroundColor='rgb(8, 71, 69)'"
onclick = "handleGroupApproval('${txSig}')"
>
Approve Add - Admin Tx
< / b u t t o n >
< / d i v >
< / d i v >
2025-01-08 20:26:24 -08:00
`
return approvalButtonHtml
}
2025-01-21 21:23:25 -08:00
2025-01-29 19:12:30 -08:00
if ( transactionType === "REMOVE_GROUP_ADMIN" && isSomeTypaAdmin ) {
2025-01-21 21:23:25 -08:00
const approvalButtonHtml = `
< div style = "display: flex; flex-direction: column; margin-top: 1em;" >
< p style = "color: rgb(189, 40, 40);" >
Existing $ { transactionType } Approvals : $ { uniqueApprovalCount }
< / p >
$ { tableHtml }
< div id = "approval-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button
style = "
padding : 8 px ;
background : rgb ( 54 , 7 , 7 ) ;
color : rgb ( 201 , 255 , 251 ) ;
border : 1 px solid # 333 ;
border - color : rgb ( 204 , 94 , 94 ) ;
border - radius : 5 px ;
cursor : pointer ;
"
onmouseover = "this.style.backgroundColor='rgb(50, 52, 51)'"
onmouseout = "this.style.backgroundColor='rgb(54, 7, 7)'"
onclick = "handleGroupApproval('${txSig}')"
>
Approve Remove - Admin Tx
< / b u t t o n >
< / d i v >
< / d i v >
`
return approvalButtonHtml
}
}
2025-01-29 19:12:30 -08:00
const buildApprovalTableHtml = async ( approvalTxs , getNameFunc ) => {
2025-01-21 21:23:25 -08:00
// Build a Map of adminAddress => one transaction (to handle multiple approvals from same admin)
const approvalMap = new Map ( )
for ( const tx of approvalTxs ) {
const adminAddr = tx . creatorAddress
if ( ! approvalMap . has ( adminAddr ) ) {
approvalMap . set ( adminAddr , tx )
}
}
// Turn the map into an array for iteration
const approvalArray = Array . from ( approvalMap , ( [ adminAddr , tx ] ) => ( { adminAddr , tx } ) )
// Build table rows asynchronously, since we need getNameFromAddress
const tableRows = await Promise . all (
approvalArray . map ( async ( { adminAddr , tx } ) => {
let adminName
try {
adminName = await getNameFunc ( adminAddr )
} catch ( err ) {
console . warn ( ` Error fetching name for ${ adminAddr } : ` , err )
adminName = null
}
const displayName =
adminName && adminName !== adminAddr
? adminName
: "(No registered name)"
const dateStr = new Date ( tx . timestamp ) . toLocaleString ( )
return `
< tr >
< td style = "border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565" > $ { displayName } < / t d >
< td style = "border: 1px solid rgb(255, 254, 254); padding: 4px;" > $ { dateStr } < / t d >
< / t r >
`
} )
)
// The total unique approvals = number of entries in approvalMap
const uniqueApprovalCount = approvalMap . size ;
2025-01-29 19:12:30 -08:00
// Wrap the table in a container with horizontal scroll:
2025-01-21 21:23:25 -08:00
// 1) max-width: 100% makes it fit the parent (card) width
// 2) overflow-x: auto allows scrolling if the table is too wide
const containerHtml = `
< div style = "max-width: 100%; overflow-x: auto;" >
< table style = "border: 1px solid #ccc; border-collapse: collapse; width: 100%;" >
< thead >
< tr style = "background:rgba(6, 50, 59, 0.61);" >
< th style = "border: 1px solid #ffffff; padding: 4px;" > Admin Name < / t h >
< th style = "border: 1px solid #ffffff; padding: 4px;" > Approval Time < / t h >
< / t r >
< / t h e a d >
< tbody >
$ { tableRows . join ( "" ) }
< / t b o d y >
< / t a b l e >
< / d i v >
`
// Return both the container-wrapped table and the count of unique approvals
return {
tableHtml : containerHtml ,
uniqueApprovalCount
}
2025-01-08 20:26:24 -08:00
}
2025-01-21 21:23:25 -08:00
2025-01-13 15:53:20 -08:00
const handleGroupApproval = async ( pendingSignature ) => {
2025-01-08 20:26:24 -08:00
try {
if ( ! userState . isMinterAdmin ) {
console . warn ( ` non-admin attempting to sign approval! ` )
return
}
const fee = 0.01
const adminPublicKey = await getPublicKeyByName ( userState . accountName )
2025-01-13 15:53:20 -08:00
const txGroupId = 0
const rawGroupApprovalTransaction = await createGroupApprovalTransaction ( adminPublicKey , pendingSignature , txGroupId , fee )
2025-01-08 20:26:24 -08:00
const signedGroupApprovalTransaction = await qortalRequest ( {
action : "SIGN_TRANSACTION" ,
unsignedBytes : rawGroupApprovalTransaction
} )
2025-01-13 15:53:20 -08:00
let txToProcess = signedGroupApprovalTransaction
2025-01-08 20:26:24 -08:00
const processGroupApprovalTx = await processTransaction ( txToProcess )
if ( processGroupApprovalTx ) {
alert ( ` transaction processed, please wait for CONFIRMATION: ${ JSON . stringify ( processGroupApprovalTx ) } ` )
} else {
alert ( ` creating tx failed for some reason ` )
}
} catch ( error ) {
console . error ( error )
throw error
}
}
const handleJoinGroup = async ( minterAddress ) => {
try {
if ( userState . accountAddress === minterAddress ) {
console . log ( ` minter user found ` )
2025-01-13 15:53:20 -08:00
const qRequestAttempt = await qortalRequest ( {
action : "JOIN_GROUP" ,
groupId : 694
} )
if ( qRequestAttempt ) {
return true
}
2025-01-08 20:26:24 -08:00
const joinerPublicKey = getPublicKeyFromAddress ( minterAddress )
2025-01-13 15:53:20 -08:00
const fee = 0.01
2025-01-08 20:26:24 -08:00
const joinGroupTransactionData = await createGroupJoinTransaction ( minterAddress , joinerPublicKey , 694 , 0 , fee )
const signedJoinGroupTransaction = await qortalRequest ( {
action : "SIGN_TRANSACTION" ,
unsignedBytes : joinGroupTransactionData
} )
let txToProcess = signedJoinGroupTransaction
const processJoinGroupTransaction = await processTransaction ( txToProcess )
if ( processJoinGroupTransaction ) {
console . warn ( ` processed JOIN_GROUP tx ` , processJoinGroupTransaction )
alert ( ` JOIN GROUP Transaction Processed Successfully, please WAIT FOR CONFIRMATION txData: ${ JSON . stringify ( processJoinGroupTransaction ) } ` )
}
} else {
console . warn ( ` user is not the minter ` )
return ''
}
} catch ( error ) {
throw error
}
}
2025-01-06 18:43:54 -08:00
const getMinterAvatar = async ( minterName ) => {
const avatarUrl = ` /arbitrary/THUMBNAIL/ ${ minterName } /qortal_avatar `
try {
const response = await fetch ( avatarUrl , { method : 'HEAD' } )
if ( response . ok ) {
return ` <img src=" ${ avatarUrl } " alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;"> `
} else {
return ''
}
} catch ( error ) {
console . error ( 'Error checking avatar availability:' , error )
return ''
}
}
2025-01-22 01:52:41 -05:00
const getNewestCommentTimestamp = async ( cardIdentifier ) => {
try {
// fetchCommentsForCard returns resources each with at least 'created' or 'updated'
const comments = await fetchCommentsForCard ( cardIdentifier )
if ( ! comments || comments . length === 0 ) {
// No comments => fallback to 0 (or card's own date, if you like)
return 0
}
// The newest can be determined by comparing 'updated' or 'created'
const newestTimestamp = comments . reduce ( ( acc , c ) => {
const cTime = c . updated || c . created || 0
return ( cTime > acc ) ? cTime : acc
} , 0 )
return newestTimestamp
} catch ( err ) {
console . error ( 'Failed to get newest comment timestamp:' , err )
return 0
}
}
2025-01-04 20:28:26 -08:00
2024-12-12 17:23:34 -08:00
// Create the overall Minter Card HTML -----------------------------------------------
2025-02-03 09:34:23 -08:00
const createCardHTML = async ( cardData , pollResults , cardIdentifier , commentCount , cardUpdatedTime , bgColor , address , isExistingMinter = false ) => {
2025-01-29 19:12:30 -08:00
const { header , content , links , creator , creatorAddress , timestamp , poll } = cardData
2024-12-24 00:27:17 -08:00
const formattedDate = cardUpdatedTime ? new Date ( cardUpdatedTime ) . toLocaleString ( ) : new Date ( timestamp ) . toLocaleString ( )
2024-12-20 22:07:18 -08:00
const avatarHtml = await getMinterAvatar ( creator )
2024-12-11 14:40:32 -08:00
const linksHTML = links . map ( ( link , index ) => `
2024-12-28 22:49:18 -08:00
< button onclick = "openLinksModal('${link}')" >
2024-12-11 14:40:32 -08:00
$ { ` Link ${ index + 1 } - ${ link } ` }
< / b u t t o n >
2024-12-28 22:49:18 -08:00
` ).join("")
const minterGroupMembers = await fetchMinterGroupMembers ( )
const minterAdmins = await fetchMinterGroupAdmins ( )
2025-01-22 01:52:41 -05:00
const { adminYes = 0 , adminNo = 0 , minterYes = 0 , minterNo = 0 , totalYes = 0 , totalNo = 0 , totalYesWeight = 0 , totalNoWeight = 0 , detailsHtml , userVote } = await processPollData ( pollResults , minterGroupMembers , minterAdmins , creator , cardIdentifier )
2024-12-28 22:49:18 -08:00
createModal ( 'links' )
createModal ( 'poll-details' )
2024-12-11 14:40:32 -08:00
2025-02-03 09:34:23 -08:00
const inviteButtonHtml = isExistingMinter ? "" : await checkAndDisplayInviteButton ( adminYes , creator , cardIdentifier )
2025-01-08 20:26:24 -08:00
let inviteHtmlAdd = ( inviteButtonHtml ) ? inviteButtonHtml : ''
let finalBgColor = bgColor
let invitedText = "" // for "INVITED" label if found
2025-01-22 01:52:41 -05:00
const addressInfo = await getAddressInfo ( address )
const penaltyText = addressInfo . blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
const adjustmentText = addressInfo . blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
2025-01-08 20:26:24 -08:00
try {
2025-01-22 01:52:41 -05:00
const invites = await fetchGroupInvitesByAddress ( address )
2025-01-08 20:26:24 -08:00
const hasMinterInvite = invites . some ( ( invite ) => invite . groupId === 694 )
2025-01-22 01:52:41 -05:00
if ( userVote === 0 ) {
2025-01-29 19:12:30 -08:00
finalBgColor = "rgba(1, 65, 39, 0.41)" ; // or any green you want
2025-01-22 01:52:41 -05:00
} else if ( userVote === 1 ) {
2025-01-29 19:12:30 -08:00
finalBgColor = "rgba(107, 3, 3, 0.3)" ; // or any red you want
2025-02-03 09:34:23 -08:00
} else if ( isExistingMinter ) {
finalBgColor = "rgb(99, 99, 99)"
invitedText = ` <h4 style="color:rgb(135, 55, 16); margin-bottom: 0.5em;">EXISTING MINTER</h4> `
2025-01-22 01:52:41 -05:00
} else if ( hasMinterInvite ) {
2025-01-08 20:26:24 -08:00
// If so, override background color & add an "INVITED" label
finalBgColor = "black" ;
invitedText = ` <h4 style="color: gold; margin-bottom: 0.5em;">INVITED</h4> `
if ( userState . accountName === creator ) { //Check also if the creator is the user, and display the join group button if so.
inviteHtmlAdd = `
< div id = "join-button-container-${cardIdentifier}" style = "margin-top: 1em;" >
< button
2025-01-29 19:12:30 -08:00
style = "padding: 8px; background: rgb(37, 99, 44); color:rgb(240, 240, 240); border: 1px solid rgb(255, 255, 255); border-radius: 5px; cursor: pointer;"
2025-01-08 20:26:24 -08:00
onmouseover = "this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout = "this.style.backgroundColor='rgb(37, 99, 44) '"
onclick = "handleJoinGroup('${userState.accountAddress}')" >
2025-01-13 15:53:20 -08:00
Join MINTER Group
2025-01-08 20:26:24 -08:00
< / b u t t o n >
< / d i v >
`
} else {
2025-01-13 15:53:20 -08:00
console . log ( ` user is not the minter... NOT displaying any join button ` )
2025-01-08 20:26:24 -08:00
inviteHtmlAdd = ''
}
}
//do not display invite button as they're already invited. Create a join button instead.
} catch ( error ) {
console . error ( "Error checking invites for user:" , error )
}
2025-01-04 20:28:26 -08:00
2024-12-11 14:40:32 -08:00
return `
2025-01-08 20:26:24 -08:00
< div class = "minter-card" style = "background-color: ${finalBgColor}" >
2024-12-11 14:40:32 -08:00
< div class = "minter-card-header" >
2024-12-20 22:07:18 -08:00
$ { avatarHtml }
2025-01-22 01:52:41 -05:00
< h3 > $ { creator } - Level $ { addressInfo . level } < / h 3 >
2024-12-11 14:40:32 -08:00
< p > $ { header } < / p >
2025-01-22 01:52:41 -05:00
$ { penaltyText } $ { adjustmentText } $ { invitedText }
2024-12-11 14:40:32 -08:00
< / d i v >
2025-01-04 20:28:26 -08:00
< div class = "support-header" > < h5 > USER ' S POST < / h 5 > < / d i v >
2024-12-11 14:40:32 -08:00
< div class = "info" >
$ { content }
< / d i v >
2025-01-04 20:28:26 -08:00
< div class = "support-header" > < h5 > USER ' S LINKS < / h 5 > < / d i v >
2024-12-11 14:40:32 -08:00
< div class = "info-links" >
$ { linksHTML }
< / d i v >
2025-01-04 20:28:26 -08:00
< div class = "results-header support-header" > < h5 > CURRENT SUPPORT RESULTS < / h 5 > < / d i v >
2024-12-11 14:40:32 -08:00
< div class = "minter-card-results" >
2024-12-28 22:49:18 -08: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-04 20:28:26 -08:00
$ { inviteHtmlAdd }
2024-12-11 14:40:32 -08:00
< div class = "admin-results" >
< span class = "admin-yes" > Admin Yes : $ { adminYes } < / s p a n >
< span class = "admin-no" > Admin No : $ { adminNo } < / s p a n >
< / d i v >
< div class = "minter-results" >
< span class = "minter-yes" > Minter Yes : $ { minterYes } < / s p a n >
< span class = "minter-no" > Minter No : $ { minterNo } < / s p a n >
< / d i v >
< div class = "total-results" >
< span class = "total-yes" > Total Yes : $ { totalYes } < / s p a n >
2024-12-20 22:07:18 -08:00
< span class = "total-yes" > Weight : $ { totalYesWeight } < / s p a n >
2024-12-11 14:40:32 -08:00
< span class = "total-no" > Total No : $ { totalNo } < / s p a n >
2024-12-20 22:07:18 -08:00
< span class = "total-no" > Weight : $ { totalNoWeight } < / s p a n >
2024-12-11 14:40:32 -08:00
< / d i v >
< / d i v >
2025-01-04 20:28:26 -08:00
< div class = "support-header" > < h5 > SUPPORT ACTION FOR < / h 5 > < h 5 s t y l e = " c o l o r : # f f a e 4 2 ; " > $ { c r e a t o r } < / h 5 >
2024-12-20 22:07:18 -08: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-11 14:40:32 -08:00
< div class = "actions" >
< div class = "actions-buttons" >
2024-12-11 18:32:48 -08: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-26 20:06:51 -08:00
< button class = "comment" id = "comment-button-${cardIdentifier}" data - comment - count = "${commentCount}" onclick = "toggleComments('${cardIdentifier}')" > COMMENTS ( $ { commentCount } ) < / b u t t o n >
2024-12-11 18:32:48 -08:00
< button class = "no" onclick = "voteNoOnPoll('${poll}')" > NO < / b u t t o n >
2024-12-11 14:40:32 -08: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 >
< textarea id = "new-comment-${cardIdentifier}" placeholder = "Write a comment..." style = "width: 100%; margin-top: 10px;" > < / t e x t a r e a >
< button onclick = "postComment('${cardIdentifier}')" > Post Comment < / b u t t o n >
< / d i v >
2024-12-20 22:07:18 -08:00
< p style = "font-size: 0.75rem; margin-top: 3vh; color: #4496a1" > By : $ { creator } - $ { formattedDate } < / p >
2024-12-11 14:40:32 -08:00
< / d i v >
2025-01-01 13:47:45 -08:00
`
2024-12-11 14:40:32 -08:00
}