Massive refactoring to search/display code on multiple boards, and many other features. More changes coming soon.
This commit is contained in:
parent
10f8230619
commit
5a35fb0d07
@ -59,6 +59,13 @@ const loadAddRemoveAdminPage = async () => {
|
||||
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
|
||||
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
|
||||
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
|
||||
<option value="0">Show All</option>
|
||||
<option value="1">Last 1 day</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30" selected>Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 1rem"">
|
||||
<!-- We'll fill this with existing proposal cards -->
|
||||
@ -80,8 +87,8 @@ const loadAddRemoveAdminPage = async () => {
|
||||
// proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error checking for existing card:", error)
|
||||
alert("Failed to check for existing card. Please try again.")
|
||||
console.error("Error opening propose form", error)
|
||||
alert("Failed to open proposal form. Please try again.")
|
||||
}
|
||||
})
|
||||
|
||||
@ -126,50 +133,10 @@ const toggleProposeButton = () => {
|
||||
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
}
|
||||
|
||||
let addAdminTxs
|
||||
let remAdminTxs
|
||||
|
||||
const fetchAllARTxData = async () => {
|
||||
const addAdmTx = "ADD_GROUP_ADMIN"
|
||||
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
||||
|
||||
const filterAddTransactions = (rawTransactions) => {
|
||||
// Group transactions by member
|
||||
const memberTxMap = rawTransactions.reduce((map, tx) => {
|
||||
if (!map[tx.member]) {
|
||||
map[tx.member] = []
|
||||
}
|
||||
map[tx.member].push(tx)
|
||||
return map
|
||||
}, {})
|
||||
|
||||
// Filter out members with both pending and non-pending transactions
|
||||
return Object.values(memberTxMap)
|
||||
.filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING'))
|
||||
.flat()
|
||||
// .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
|
||||
// txs.some(tx => tx.approvalStatus !== 'PENDING')))
|
||||
// .flat()
|
||||
}
|
||||
|
||||
const filterRemoveTransactions = (rawTransactions) => {
|
||||
// Group transactions by member
|
||||
const adminTxMap = rawTransactions.reduce((map, tx) => {
|
||||
if (!map[tx.admin]) {
|
||||
map[tx.admin] = []
|
||||
}
|
||||
map[tx.admin].push(tx)
|
||||
return map
|
||||
}, {})
|
||||
|
||||
// Filter out members with both pending and non-pending transactions
|
||||
return Object.values(adminTxMap)
|
||||
.filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
|
||||
txs.some(tx => tx.approvalStatus !== 'PENDING')))
|
||||
.flat()
|
||||
}
|
||||
|
||||
// Fetch ban transactions
|
||||
const allAddTxs = await searchTransactions({
|
||||
txTypes: [addAdmTx],
|
||||
confirmationStatus: 'CONFIRMED',
|
||||
@ -180,11 +147,7 @@ const fetchAllARTxData = async () => {
|
||||
blockLimit: 0,
|
||||
txGroupId: 694,
|
||||
})
|
||||
// Filter out 'PENDING'
|
||||
addAdminTxs = filterAddTransactions(allAddTxs)
|
||||
console.warn('addAdminTxData (no PENDING nor past+PENDING):', addAdminTxs)
|
||||
|
||||
// Fetch kick transactions
|
||||
const allRemTxs = await searchTransactions({
|
||||
txTypes: [remAdmTx],
|
||||
confirmationStatus: 'CONFIRMED',
|
||||
@ -195,10 +158,54 @@ const fetchAllARTxData = async () => {
|
||||
blockLimit: 0,
|
||||
txGroupId: 694,
|
||||
})
|
||||
// Filter out 'PENDING'
|
||||
remAdminTxs = filterRemoveTransactions(allRemTxs)
|
||||
console.warn('remAdminTxData (no PENDING nor past+PENDING):', remAdminTxs)
|
||||
|
||||
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs)
|
||||
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs)
|
||||
|
||||
// We are going to keep all transactions in order to filter more accurately for display purposes.
|
||||
console.log('Final addAdminTxs:', finalAddTxs);
|
||||
console.log('Pending addAdminTxs:', pendingAddTxs);
|
||||
console.log('Final remAdminTxs:', finalRemTxs);
|
||||
console.log('Pending remAdminTxs:', pendingRemTxs);
|
||||
|
||||
return {
|
||||
finalAddTxs,
|
||||
pendingAddTxs,
|
||||
finalRemTxs,
|
||||
pendingRemTxs,
|
||||
}
|
||||
}
|
||||
|
||||
function partitionAddTransactions(rawTransactions) {
|
||||
const finalAddTxs = []
|
||||
const pendingAddTxs = []
|
||||
|
||||
for (const tx of rawTransactions) {
|
||||
if (tx.approvalStatus === 'PENDING') {
|
||||
pendingAddTxs.push(tx)
|
||||
} else {
|
||||
finalAddTxs.push(tx)
|
||||
}
|
||||
}
|
||||
|
||||
return { finalAddTxs, pendingAddTxs };
|
||||
}
|
||||
|
||||
function partitionRemoveTransactions(rawTransactions) {
|
||||
const finalRemTxs = []
|
||||
const pendingRemTxs = []
|
||||
|
||||
for (const tx of rawTransactions) {
|
||||
if (tx.approvalStatus === 'PENDING') {
|
||||
pendingRemTxs.push(tx)
|
||||
} else {
|
||||
finalRemTxs.push(tx)
|
||||
}
|
||||
}
|
||||
|
||||
return { finalRemTxs, pendingRemTxs }
|
||||
}
|
||||
|
||||
|
||||
const displayExistingMinterAdmins = async () => {
|
||||
const adminListContainer = document.getElementById("admin-list-container")
|
||||
@ -309,9 +316,10 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
|
||||
|
||||
// Notify the user to fill out the rest
|
||||
alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => {
|
||||
|
||||
const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => {
|
||||
try {
|
||||
const response = await searchSimple(
|
||||
'BLOG_POST',
|
||||
@ -381,7 +389,7 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
|
||||
console.error("Error fetching existing AR card:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const publishARCard = async (cardIdentifierPrefix) => {
|
||||
@ -726,8 +734,8 @@ const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins)
|
||||
}
|
||||
|
||||
|
||||
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
||||
const { minterName, header, content, links, creator, timestamp, poll, promotionCard } = cardData
|
||||
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => {
|
||||
const { minterName, minterAddress='priorToAddition', header, content, links, creator, timestamp, poll, promotionCard } = cardData
|
||||
const formattedDate = new Date(timestamp).toLocaleString()
|
||||
const minterAvatar = await getMinterAvatar(minterName)
|
||||
const creatorAvatar = await getMinterAvatar(creator)
|
||||
@ -744,7 +752,7 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
||||
// showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins)
|
||||
|
||||
if (typeof promotionCard === 'boolean') {
|
||||
showPromotionCard = promotionCard;
|
||||
showPromotionCard = promotionCard
|
||||
} else if (typeof promotionCard === 'string') {
|
||||
// Could be "true" or "false" or something else
|
||||
const lower = promotionCard.trim().toLowerCase()
|
||||
@ -790,41 +798,63 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
||||
let altText = ''
|
||||
const verifiedName = await validateMinterName(minterName)
|
||||
|
||||
if (verifiedName) {
|
||||
if (verifiedName && !illegalDuplicate) {
|
||||
const accountInfo = await getNameInfo(verifiedName)
|
||||
const accountAddress = accountInfo.owner
|
||||
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
|
||||
const adminAddresses = minterAdmins.map(m => m.member)
|
||||
const existingAdmin = adminAddresses.includes(accountAddress)
|
||||
const existingMinter = minterGroupAddresses.includes(accountAddress)
|
||||
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
|
||||
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
|
||||
actionsHtml = actionsHtmlCheck
|
||||
|
||||
if (!addAdminTxs || !remAdminTxs) {
|
||||
await fetchAllARTxData()
|
||||
}
|
||||
const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData()
|
||||
|
||||
if (addAdminTxs.some((addTx) => addTx.groupId === 694 && addTx.member === accountAddress)){
|
||||
console.warn(`account was already adminified(PROMOTED), displaying as such...`)
|
||||
const confirmedAdd = finalAddTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.member === accountAddress
|
||||
)
|
||||
const userPendingAdd = pendingAddTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.member === accountAddress
|
||||
)
|
||||
const confirmedRemove = finalRemTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.admin === accountAddress
|
||||
)
|
||||
const userPendingRemove = pendingRemTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.admin === accountAddress
|
||||
)
|
||||
|
||||
// If user is definitely admin (finalAdd) and not pending removal
|
||||
if (confirmedAdd && !userPendingRemove && existingAdmin) {
|
||||
console.warn(`account was already admin, final. no add/remove pending.`);
|
||||
cardColorCode = 'rgb(3, 11, 24)'
|
||||
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`
|
||||
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
|
||||
actionsHtml = ''
|
||||
// =============================================================== if 'showPromotedDemoted' is wanted to be added.
|
||||
// if (!showPromotedDemoted){
|
||||
// console.warn(`promoted/demoted show checkbox is unchecked, and card is promoted, not displaying...`)
|
||||
// return ''
|
||||
// }
|
||||
}
|
||||
|
||||
if (remAdminTxs.some((remTx) => remTx.groupId === 694 && remTx.admin === accountAddress)){
|
||||
console.warn(`account was already UNadminified(DEMOTED), displaying as such...`)
|
||||
if (confirmedAdd && userPendingRemove && existingAdmin) {
|
||||
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
|
||||
}
|
||||
|
||||
// If user has a final "remove" and no pending additions or removals
|
||||
if (confirmedRemove && !userPendingAdd && existingMinter) {
|
||||
console.warn(`account was demoted, final. no add pending, existingMinter.`);
|
||||
cardColorCode = 'rgb(29, 4, 6)'
|
||||
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
|
||||
actionsHtml = ''
|
||||
|
||||
// if (!showPromotedDemoted) {
|
||||
// console.warn(`promoted/demoted show checkbox is unchecked, card is demoted, not displaying...`)
|
||||
// return ''
|
||||
// }
|
||||
}
|
||||
|
||||
// If user has both final remove and pending add, do something else
|
||||
if (confirmedRemove && userPendingAdd && existingMinter) {
|
||||
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
|
||||
// Possibly show "DEMOTED but re-add in progress" or something
|
||||
}
|
||||
|
||||
} else if ( verifiedName && illegalDuplicate) {
|
||||
console.warn(`illegalDuplicate detected (this card was somehow allowed to be published twice, keeping newest as active to prevent issues with old cards and updates, but displaying without actions...)`)
|
||||
cardColorCode = 'rgb(82, 81, 81)'
|
||||
altText = `<h4 style="color:rgb(21, 30, 39); margin-bottom: 0.5em;">DUPLICATE (diplayed for data only)</h4>`
|
||||
actionsHtml = ''
|
||||
} else {
|
||||
console.warn(`name could not be validated, not setting actionsHtml`)
|
||||
actionsHtml = ''
|
||||
|
@ -83,6 +83,13 @@ const loadAdminBoardPage = async () => {
|
||||
<option value="least-votes">Least Votes</option>
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</select>
|
||||
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
|
||||
<option value="0">Show All</option>
|
||||
<option value="1">Last 1 day</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30" selected>Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
<div class="show-card-checkbox" style="margin-top: 1em;">
|
||||
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
|
||||
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
|
||||
@ -327,8 +334,20 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
||||
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
|
||||
|
||||
let afterTime = null
|
||||
const timeRangeSelect = document.getElementById("time-range-select")
|
||||
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()}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0)
|
||||
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0, 0, '', false, true, afterTime)
|
||||
|
||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
|
||||
@ -379,12 +398,13 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
const latestCardsMap = new Map()
|
||||
|
||||
validCardsWithData.forEach(({ card, decryptedCardData }) => {
|
||||
const timestamp = card.updated || card.created || 0
|
||||
const timestamp = card.created || 0
|
||||
const existingCard = latestCardsMap.get(card.identifier)
|
||||
|
||||
if (!existingCard || timestamp > (existingCard.card.updated || existingCard.card.created || 0)) {
|
||||
if (!existingCard || timestamp < (existingCard.card.updated || existingCard.card.created || 0)) {
|
||||
latestCardsMap.set(card.identifier, { card, decryptedCardData })
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const uniqueValidCards = Array.from(latestCardsMap.values())
|
||||
@ -396,13 +416,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
const obtainedMinterName = decryptedCardData.minterName
|
||||
// Only check for cards that are NOT topic-based cards
|
||||
if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') {
|
||||
const cardTimestamp = card.updated || card.created || 0
|
||||
|
||||
if (obtainedMinterName) {
|
||||
const existingEntry = mostRecentCardsMap.get(obtainedMinterName)
|
||||
|
||||
// Replace only if the current card is more recent
|
||||
if (!existingEntry || cardTimestamp > (existingEntry.card.updated || existingEntry.card.created || 0)) {
|
||||
if (!existingEntry) {
|
||||
mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData })
|
||||
}
|
||||
}
|
||||
@ -414,7 +432,7 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
})
|
||||
|
||||
// Convert the map into an array of final cards
|
||||
const finalCards = Array.from(mostRecentCardsMap.values());
|
||||
const finalCards = Array.from(mostRecentCardsMap.values())
|
||||
|
||||
let selectedSort = 'newest'
|
||||
const sortSelect = document.getElementById('sort-select')
|
||||
@ -1037,7 +1055,7 @@ const processQortalLinkForRendering = async (link) => {
|
||||
return link
|
||||
}
|
||||
|
||||
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
|
||||
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, nameIsActuallyAddress = false) => {
|
||||
const latestBlockInfo = await getLatestBlockInfo()
|
||||
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
||||
let minAdminCount
|
||||
@ -1052,10 +1070,15 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
|
||||
minAdminCount = Math.round(fortyPercent)
|
||||
console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`)
|
||||
}
|
||||
if (isBlockPassed && userState.isMinterAdmin) {
|
||||
if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
|
||||
console.warn(`feature trigger has passed, checking for approval requirements`)
|
||||
const addressInfo = await getNameInfo(name)
|
||||
const address = addressInfo.owner
|
||||
let address
|
||||
if (!nameIsActuallyAddress){
|
||||
const nameInfo = await getNameInfo(name)
|
||||
address = nameInfo.owner
|
||||
} else {
|
||||
address = name
|
||||
}
|
||||
const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK")
|
||||
const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN")
|
||||
|
||||
@ -1068,7 +1091,7 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (adminYes >= minAdminCount && userState.isMinterAdmin) {
|
||||
if (adminYes >= minAdminCount && (userState.isMinterAdmin || userState.isAdmin)) {
|
||||
const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier)
|
||||
return removeButtonHtml
|
||||
} else{
|
||||
@ -1098,6 +1121,8 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
|
||||
|
||||
const handleKickMinter = async (minterName) => {
|
||||
try {
|
||||
isAddress = await getAddressInfo(minterName)
|
||||
|
||||
// Optional block check
|
||||
let txGroupId = 0
|
||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||
@ -1108,8 +1133,14 @@ const handleKickMinter = async (minterName) => {
|
||||
}
|
||||
|
||||
// Get the minter address from name info
|
||||
let minterAddress
|
||||
if (!isAddress){
|
||||
const minterNameInfo = await getNameInfo(minterName)
|
||||
const minterAddress = minterNameInfo?.owner
|
||||
minterAddress = minterNameInfo?.owner
|
||||
} else {
|
||||
minterAddress = minterName
|
||||
}
|
||||
|
||||
if (!minterAddress) {
|
||||
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
|
||||
return
|
||||
@ -1150,6 +1181,7 @@ const handleKickMinter = async (minterName) => {
|
||||
}
|
||||
|
||||
const handleBanMinter = async (minterName) => {
|
||||
isAddress = await getAddressInfo(minterName)
|
||||
try {
|
||||
let txGroupId = 0
|
||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||
@ -1161,9 +1193,13 @@ const handleBanMinter = async (minterName) => {
|
||||
console.log(`featureTrigger block is passed, using txGroupId 694`)
|
||||
txGroupId = 694
|
||||
}
|
||||
|
||||
let minterAddress
|
||||
if (!isAddress) {
|
||||
const minterNameInfo = await getNameInfo(minterName)
|
||||
const minterAddress = minterNameInfo?.owner
|
||||
} else {
|
||||
minterAddress = minterName
|
||||
}
|
||||
|
||||
if (!minterAddress) {
|
||||
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
|
||||
@ -1236,7 +1272,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
||||
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
|
||||
|
||||
const isUndefinedUser = (minterName === 'undefined')
|
||||
const isUndefinedUser = (minterName === 'undefined' || minterName === 'null')
|
||||
|
||||
const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode')
|
||||
|
||||
@ -1254,7 +1290,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
|
||||
|
||||
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
|
||||
<div class="support-header"><h5> REGARDING (Topic): </h5></div>
|
||||
<div class="support-header"><h5> REGARDING (Topic / Address): </h5></div>
|
||||
<h3>${minterName}` :
|
||||
`
|
||||
<div class="support-header"><h5> REGARDING (Name): </h5></div>
|
||||
@ -1274,16 +1310,23 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
let adjustmentText = ''
|
||||
const verifiedName = await validateMinterName(minterName)
|
||||
let levelText = '</h3>'
|
||||
const addressVerification = await getAddressInfo(minterName)
|
||||
const verifiedAddress = addressVerification.address
|
||||
|
||||
if (verifiedName || verifiedAddress) {
|
||||
let accountInfo
|
||||
if (!verifiedAddress){
|
||||
accountInfo = await getNameInfo(verifiedName)
|
||||
}
|
||||
|
||||
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
|
||||
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress)
|
||||
|
||||
if (verifiedName) {
|
||||
const accountInfo = await getNameInfo(verifiedName)
|
||||
const accountAddress = accountInfo.owner
|
||||
const addressInfo = await getAddressInfo(accountAddress)
|
||||
levelText = ` - Level ${addressInfo.level}</h3>`
|
||||
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
|
||||
penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
|
||||
adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
|
||||
const removeActionsHtml = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
|
||||
const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
|
||||
showRemoveHtml = removeActionsHtml
|
||||
if (userVote === 0) {
|
||||
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
|
||||
|
@ -28,10 +28,10 @@ async function loadMinterAdminToolsPage() {
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
<div><h2>No Functionality Here Yet</h2></div>
|
||||
<div><h2>COMING SOON...</h2></div>
|
||||
<div>
|
||||
<p>This page is still under development. Until the final Mintership proposal modifications are made, and the MINTER group is transferred to null, there is no need for this page's functionality. The page will be updated when the final modifications are made.</p>
|
||||
<p> This page until then is simply a placeholder.</p>
|
||||
<p>This page will have functionality to assist the Minter Admins in performing their duties. It will display all pending transactions (of any kind they can approve/deny) along with that ability. It can also be utilized to obtain more in-depth information about existing accounts.</p>
|
||||
<p> The page will be getting a significant overhaul in the near(ish) future, as the MINTER group is now owned by null, and we are past the 'temporary state' we were in for much longer than planned.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -37,6 +37,13 @@ const loadMinterBoardPage = async () => {
|
||||
<option value="least-votes">Least Votes</option>
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</select>
|
||||
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
|
||||
<option value="0">Show All</option>
|
||||
<option value="1">Last 1 day</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30" selected>Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<form id="publish-card-form">
|
||||
@ -127,6 +134,11 @@ const loadMinterBoardPage = async () => {
|
||||
await publishCard(minterCardIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("time-range-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
@ -149,11 +161,11 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
|
||||
}
|
||||
try {
|
||||
if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){
|
||||
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
|
||||
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
|
||||
const minterName = await searchSimpleResults.name
|
||||
return minterName
|
||||
} else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) {
|
||||
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
|
||||
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
|
||||
const publisherName = searchSimpleResults.name
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
@ -161,103 +173,204 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
})
|
||||
let nameInvalid = false
|
||||
const minterName = cardDataResponse.minterName
|
||||
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
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const processMinterCards = async (validMinterCards) => {
|
||||
const latestCardsMap = new Map()
|
||||
|
||||
// Deduplicate by identifier, keeping the most recent
|
||||
validMinterCards.forEach(card => {
|
||||
const timestamp = card.updated || card.created || 0
|
||||
const existingCard = latestCardsMap.get(card.identifier)
|
||||
|
||||
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
|
||||
latestCardsMap.set(card.identifier, card)
|
||||
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)
|
||||
})
|
||||
|
||||
// Convert Map back to array
|
||||
const uniqueValidCards = Array.from(latestCardsMap.values())
|
||||
// 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
|
||||
})
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
|
||||
const minterNameMap = new Map()
|
||||
// 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
|
||||
}
|
||||
|
||||
// For each card, extract minterName safely
|
||||
for (const card of uniqueValidCards) {
|
||||
let minterName
|
||||
// push them all to output
|
||||
output.push(...group)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// no semicolons, using arrow functions
|
||||
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, [])
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// no semicolons, arrow functions
|
||||
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.`)
|
||||
continue
|
||||
}
|
||||
// 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 {
|
||||
// If this throws, we catch below and skip
|
||||
minterName = await extractMinterCardsMinterName(card.identifier)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Skipping card ${card.identifier} because extractMinterCardsMinterName failed:`,
|
||||
error
|
||||
)
|
||||
continue // Skip this card and move on
|
||||
}
|
||||
|
||||
console.log(`minterName`, minterName)
|
||||
// Next, get minterNameInfo
|
||||
const minterNameInfo = await getNameInfo(minterName)
|
||||
if (!minterNameInfo) {
|
||||
console.warn(`minterNameInfo is null for minter: ${minterName}, skipping card.`)
|
||||
masterMinterName = await extractMinterCardsMinterName(masterCard.identifier)
|
||||
} catch (err) {
|
||||
console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err)
|
||||
continue
|
||||
}
|
||||
|
||||
const minterAddress = minterNameInfo.owner
|
||||
// Validate the address
|
||||
const addressValid = await getAddressInfo(minterAddress)
|
||||
if (!minterAddress || !addressValid) {
|
||||
console.warn(`minterAddress invalid or missing for: ${minterName}, skipping card.`, minterAddress)
|
||||
continue
|
||||
// Store an object with the minterName we extracted, plus all cards in that group
|
||||
nameGroups.push({
|
||||
minterName: masterMinterName,
|
||||
cards: group // includes the master & updates
|
||||
})
|
||||
}
|
||||
|
||||
// If this is a 'regular' minter card, skip if user is already a minter
|
||||
if (!card.identifier.includes('QM-AR-card')) {
|
||||
if (minterGroupAddresses.includes(minterAddress)) {
|
||||
console.log(
|
||||
`existing minter found or fake name detected. Not including minter card: ${card.identifier}`
|
||||
)
|
||||
continue
|
||||
// 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, [])
|
||||
}
|
||||
combinedMap.get(mName).push(...entry.cards)
|
||||
}
|
||||
|
||||
// Keep only the most recent card for each minterName
|
||||
const existingCard = minterNameMap.get(minterName)
|
||||
const cardTimestamp = card.updated || card.created || 0
|
||||
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
|
||||
return combinedMap
|
||||
}
|
||||
|
||||
if (!existingCard || cardTimestamp > existingTimestamp) {
|
||||
minterNameMap.set(minterName, card)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert minterNameMap to final array
|
||||
const finalCards = []
|
||||
const seenMinterNames = new Set()
|
||||
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 [minterName, card] of minterNameMap.entries()) {
|
||||
if (!seenMinterNames.has(minterName)) {
|
||||
finalCards.push(card)
|
||||
seenMinterNames.add(minterName)
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp descending
|
||||
finalCards.sort((a, b) => {
|
||||
const timestampA = a.updated || a.created || 0
|
||||
const timestampB = b.updated || b.created || 0
|
||||
return timestampB - timestampA
|
||||
// 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
|
||||
})
|
||||
|
||||
return finalCards
|
||||
return finalOutput
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// push them all
|
||||
finalOutput.push(...group)
|
||||
}
|
||||
// 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
|
||||
})
|
||||
|
||||
return finalOutput
|
||||
}
|
||||
|
||||
|
||||
@ -266,37 +379,51 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
const cardsContainer = document.getElementById("cards-container")
|
||||
let isARBoard = false
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
||||
if ((cardIdentifierPrefix.startsWith(`QM-AR-card`))) {
|
||||
|
||||
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
||||
isARBoard = true
|
||||
console.warn(`ARBoard determined:`, isARBoard)
|
||||
}
|
||||
let afterTime = 0
|
||||
const timeRangeSelect = document.getElementById("time-range-select")
|
||||
|
||||
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()}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0)
|
||||
// 1) Fetch raw "BLOG_POST" entries
|
||||
const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
|
||||
|
||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>"
|
||||
return
|
||||
}
|
||||
// Validate cards and filter
|
||||
// 2) Validate structure
|
||||
const validatedCards = await Promise.all(
|
||||
response.map(async card => {
|
||||
response.map(async (card) => {
|
||||
const isValid = await validateCardStructure(card)
|
||||
return isValid ? card : null
|
||||
})
|
||||
)
|
||||
const validCards = validatedCards.filter(card => card !== null)
|
||||
const validCards = validatedCards.filter((card) => card !== null)
|
||||
|
||||
if (validCards.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
|
||||
return
|
||||
}
|
||||
const finalCards = await processMinterCards(validCards)
|
||||
// Additional logic for ARBoard or MinterCards
|
||||
const finalCards = isARBoard
|
||||
? await processARBoardCards(validCards)
|
||||
: await processMinterBoardCards(validCards)
|
||||
|
||||
// finalCards is already sorted by newest (timestamp) by processMinterCards.
|
||||
// We'll re-sort if "Name" or "Recent Comments" is selected.
|
||||
|
||||
// Grab the selected sort from the dropdown (if it exists).
|
||||
// Sort finalCards according to selectedSort
|
||||
let selectedSort = 'newest'
|
||||
const sortSelect = document.getElementById('sort-select')
|
||||
if (sortSelect) {
|
||||
@ -304,225 +431,107 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
}
|
||||
|
||||
if (selectedSort === 'name') {
|
||||
// Sort alphabetically by the creator's name
|
||||
finalCards.sort((a, b) => {
|
||||
const nameA = a.name?.toLowerCase() || ''
|
||||
const nameB = b.name?.toLowerCase() || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
} else if (selectedSort === 'recent-comments') {
|
||||
// We need each card's newest comment timestamp for sorting
|
||||
// If you need the newest comment timestamp
|
||||
for (let card of finalCards) {
|
||||
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
|
||||
}
|
||||
// Then sort descending by newest comment
|
||||
finalCards.sort((a, b) =>
|
||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
)
|
||||
} else if (selectedSort === 'least-votes') {
|
||||
// For each card, fetch its poll data so we know how many admin + minter votes it has.
|
||||
// Store those values on the card object so we can sort on them.
|
||||
// Sort ascending by admin votes; if there's a tie, sort ascending by minter votes.
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
// For each card, fetch the poll data & store counts on the card object.
|
||||
for (const card of finalCards) {
|
||||
try {
|
||||
// We must fetch the card data from QDN to get the `poll` name
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
})
|
||||
// If the card or poll is missing, skip
|
||||
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
|
||||
)
|
||||
// Store the totals so we can sort on them
|
||||
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)
|
||||
// If something fails, default to zero so it sorts "lowest"
|
||||
card._adminVotes = 0
|
||||
card._adminYes = 0
|
||||
card._minterVotes = 0
|
||||
card._minterYes = 0
|
||||
}
|
||||
}
|
||||
// Now that each card has _adminVotes + _minterVotes, do an ascending sort:
|
||||
finalCards.sort((a, b) => {
|
||||
// admin votes ascending
|
||||
const diffAdminTotal = a._adminVotes - b._adminVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
// admin YES ascending
|
||||
const diffAdminYes = a._adminYes - b._adminYes
|
||||
if (diffAdminYes !== 0) return diffAdminYes
|
||||
// minter votes ascending
|
||||
const diffMinterTotal = a._minterVotes - b._minterVotes
|
||||
if (diffMinterTotal !== 0) return diffMinterTotal
|
||||
// minter YES ascending
|
||||
return a._minterYes - b._minterYes
|
||||
})
|
||||
await applyVoteSortingData(finalCards, /* ascending= */ true)
|
||||
} else if (selectedSort === 'most-votes') {
|
||||
// Fetch poll data & store admin + minter votes as before.
|
||||
// Sort descending by admin votes; if there's a tie, sort descending by minter votes.
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
await applyVoteSortingData(finalCards, /* ascending= */ false)
|
||||
}
|
||||
// else 'newest' => do nothing (already sorted newest-first by your process functions).
|
||||
// Create the 'finalCardsArray' that includes the data, etc.
|
||||
let finalCardsArray = []
|
||||
|
||||
for (const card of finalCards) {
|
||||
try {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
identifier: card.identifier
|
||||
})
|
||||
|
||||
if (!cardDataResponse || !cardDataResponse.poll) {
|
||||
card._adminVotes = 0
|
||||
card._adminYes = 0
|
||||
card._minterVotes = 0
|
||||
card._minterYes = 0
|
||||
// skip
|
||||
console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`)
|
||||
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
|
||||
}
|
||||
}
|
||||
// Sort descending by admin votes, then minter votes
|
||||
finalCards.sort((a, b) => {
|
||||
// admin votes descending
|
||||
const diffAdminTotal = b._adminVotes - a._adminVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
// admin YES descending
|
||||
const diffAdminYes = b._adminYes - a._adminYes
|
||||
if (diffAdminYes !== 0) return diffAdminYes
|
||||
// minter votes descending
|
||||
const diffMinterTotal = b._minterVotes - a._minterVotes
|
||||
if (diffMinterTotal !== 0) return diffMinterTotal
|
||||
// minter YES descending
|
||||
return b._minterYes - a._minterYes
|
||||
})
|
||||
}
|
||||
// else 'newest' => do nothing, finalCards stays in newest-first order
|
||||
|
||||
// Display skeleton cards immediately
|
||||
cardsContainer.innerHTML = ""
|
||||
finalCards.forEach(card => {
|
||||
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||
})
|
||||
|
||||
// Fetch and update each card
|
||||
finalCards.forEach(async card => {
|
||||
try {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
})
|
||||
|
||||
if (!cardDataResponse) {
|
||||
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
}
|
||||
|
||||
if (!cardDataResponse.poll) {
|
||||
console.warn(`Skipping card with no poll: ${card.identifier}`)
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
}
|
||||
|
||||
// Getting the poll owner address uses the same API call as public key, so takes the same time, but address will be needed later.
|
||||
// Extra validation: check poll ownership matches card publisher
|
||||
const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll)
|
||||
// Getting the card publisher address to compare instead should cause faster loading, since getting the public key by name first gets the address then converts it
|
||||
const cardPublisherAddress = await fetchOwnerAddressFromName(card.name)
|
||||
|
||||
if (pollPublisherAddress != cardPublisherAddress) {
|
||||
console.warn(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`)
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
if (pollPublisherAddress !== cardPublisherAddress) {
|
||||
console.warn(`Poll hijack attack found, discarding card ${card.identifier}`)
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(cardDataResponse.poll)
|
||||
const bgColor = generateDarkPastelBackgroundBy(card.name)
|
||||
const commentCount = await countComments(card.identifier)
|
||||
const cardUpdatedTime = card.updated || null
|
||||
|
||||
// If ARBoard, do a quick address check
|
||||
if (isARBoard) {
|
||||
const name = await getNameInfo(cardDataResponse.minterName)
|
||||
const address = name.owner
|
||||
if (minterAdminAddresses && minterGroupAddresses) {
|
||||
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
|
||||
console.warn(`Found card from ARBoard that contained a non-minter!`)
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
}
|
||||
} else if (!minterAdminAddresses || !minterGroupAddresses){
|
||||
const minterGroup = await fetchMinterGroupMembers()
|
||||
const adminGroup = await fetchMinterGroupAdmins()
|
||||
minterAdminAddresses = adminGroup.map(m => m.member)
|
||||
minterGroupAddresses = minterGroup.map(m => m.member)
|
||||
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
|
||||
console.warn(`Found card from ARBoard that contained a non-minter!`)
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
const ok = await verifyARBoardAddress(cardDataResponse.minterName)
|
||||
if (!ok) {
|
||||
console.warn(`Invalid minter address for AR board. identifier=${card.identifier}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalCardHTML = isARBoard ? // If we're calling from the ARBoard, we will create HTML with a different function.
|
||||
await createARCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor)
|
||||
:
|
||||
await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress)
|
||||
replaceSkeleton(card.identifier, finalCardHTML)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing card ${card.identifier}:`, error)
|
||||
removeSkeleton(card.identifier)
|
||||
}
|
||||
// **Push** to finalCardsArray for further processing (duplicates, etc.)
|
||||
finalCardsArray.push({
|
||||
...card,
|
||||
cardDataResponse,
|
||||
pollPublisherAddress,
|
||||
cardPublisherAddress,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`Error preparing card ${card.identifier}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
// This will now allow duplicates to be displayed, but also mark the duplicates correctly. There is one bugged identifier that must be handled opposite.
|
||||
// finalCardsArray = markDuplicates(finalCardsArray)
|
||||
|
||||
// Next, do the actual rendering:
|
||||
cardsContainer.innerHTML = ""
|
||||
for (const cardObj of finalCardsArray) {
|
||||
// Insert a skeleton first if you like
|
||||
const skeletonHTML = createSkeletonCardHTML(cardObj.identifier)
|
||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||
// 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)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error)
|
||||
@ -530,6 +539,94 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
}
|
||||
}
|
||||
|
||||
const verifyARBoardAddress = async (minterName) => {
|
||||
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) {
|
||||
console.warn("verifyARBoardAddress error:", err)
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeSkeleton = (cardIdentifier) => {
|
||||
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
|
||||
if (skeletonCard) {
|
||||
|
@ -802,9 +802,9 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => {
|
||||
}
|
||||
}
|
||||
// NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object.
|
||||
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true) => {
|
||||
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true, after=0) => {
|
||||
try {
|
||||
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}`
|
||||
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&fter=${after}`
|
||||
|
||||
if (name && !identifier && !room) {
|
||||
console.log('name only searchSimple', name)
|
||||
|
50
index.html
50
index.html
@ -42,7 +42,7 @@
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap">
|
||||
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.02b)
|
||||
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.04b)
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@ -61,12 +61,12 @@
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.02b<br></a></span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.04b<br></a></span>
|
||||
</div>
|
||||
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
|
||||
|
||||
|
||||
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">ARA BOARD</a></div>
|
||||
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">MAM Board</a></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -93,7 +93,7 @@
|
||||
<div class="item-wrapper">
|
||||
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">Minting Rights? - Start Here - MinterBoard</h2>
|
||||
<h2 class="card-title mbr-fonts-style display-2">MinterBoard</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@ -109,7 +109,7 @@
|
||||
<img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">
|
||||
Promote / Demote Minter Admins</h2>
|
||||
Minter Admin Management (MAM) Board</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@ -191,6 +191,42 @@
|
||||
|
||||
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||
v1.04beta 01-27-2025</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 card">
|
||||
<div class="text-wrapper">
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
<b><u>v1.04b Fixes</u></b>- <b>MANY fixes </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for details, too many to list here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||
v1.03beta 01-23-2025</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 card">
|
||||
<div class="text-wrapper">
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
<b><u>v1.03b Fixes</u></b>- <b>Filtering issue resolved </b> - Version 1.02 had a filtering logic modification applied to add and remove admin transactions. However, it was not changed on the REMOVE filtering, so REMOVE transactions that were PENDING were showing as COMPLETE and thus the board was displaying cards as already removed when there was only a PENDING tx. This has been resolved in 1.03b.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
@ -536,12 +572,12 @@
|
||||
<div class="title-wrapper">
|
||||
<div class="title-wrap">
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.02b)</h2>
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.04b)</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="link-wrap" href="#">
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.02beta</p>
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.04beta</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
Loading…
Reference in New Issue
Block a user