QM-Mintership v1.05.2[2]b #8
@ -163,6 +163,21 @@
|
||||
background-color: #19403d;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1vh;
|
||||
background-color: #891616;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 1vh;
|
||||
padding: 0.3vh 0.6vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background-color: #3d1919;
|
||||
}
|
||||
|
||||
/* forum-styles.css additions */
|
||||
|
||||
.message-input-section {
|
||||
|
@ -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="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:white; background-color: black;">
|
||||
<option value="newest" selected>Sort by Date</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="recent-comments">Newest Comments</option>
|
||||
<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; font-size: 1.25rem; color: white; background-color: black;">
|
||||
<option value="0">Show All</option>
|
||||
<option value="1">Last 1 day</option>
|
||||
@ -121,6 +128,12 @@ const loadAddRemoveAdminPage = async () => {
|
||||
event.preventDefault()
|
||||
await publishARCard(addRemoveIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
})
|
||||
|
||||
await featureTriggerCheck()
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
await displayExistingMinterAdmins()
|
||||
@ -133,6 +146,19 @@ const toggleProposeButton = () => {
|
||||
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
}
|
||||
|
||||
const toggleAdminTable = () => {
|
||||
const tableContainer = document.getElementById("adminTableContainer")
|
||||
const toggleBtn = document.getElementById("toggleAdminTableButton")
|
||||
|
||||
if (tableContainer.style.display === "none") {
|
||||
tableContainer.style.display = "block"
|
||||
toggleBtn.textContent = "Hide Minter Admins"
|
||||
} else {
|
||||
tableContainer.style.display = "none"
|
||||
toggleBtn.textContent = "Show Minter Admins"
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAllARTxData = async () => {
|
||||
const addAdmTx = "ADD_GROUP_ADMIN"
|
||||
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
||||
@ -216,6 +242,9 @@ const displayExistingMinterAdmins = async () => {
|
||||
// 1) Fetch addresses
|
||||
const admins = await fetchMinterGroupAdmins()
|
||||
minterAdminAddresses = admins.map(m => m.member)
|
||||
// Compute total admin count and signatures needed (40%, rounded up)
|
||||
const totalAdmins = admins.length;
|
||||
const signaturesNeeded = Math.ceil(totalAdmins * 0.40);
|
||||
let rowsHtml = "";
|
||||
for (const adminAddr of admins) {
|
||||
if (adminAddr.member === nullAddress) {
|
||||
@ -262,6 +291,22 @@ const displayExistingMinterAdmins = async () => {
|
||||
}
|
||||
// 3) Build the table
|
||||
const tableHtml = `
|
||||
<div style="text-align: center; margin-bottom: 1em;">
|
||||
<button
|
||||
id="toggleAdminTableButton"
|
||||
onclick="toggleAdminTable()"
|
||||
style="
|
||||
padding: 10px;
|
||||
background: #444;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
Show Minter Admins
|
||||
</button>
|
||||
</div>
|
||||
<div id="adminTableContainer" style="display: none;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
|
||||
@ -274,8 +319,13 @@ const displayExistingMinterAdmins = async () => {
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
adminListContainer.innerHTML = tableHtml
|
||||
adminListContainer.innerHTML = `
|
||||
<h3 style="color:rgb(212, 212, 212);">Existing Minter Admins: ${totalAdmins}</h3>
|
||||
<h4 style="color:rgb(212, 212, 212);">Signatures for Group Approval (40%): ${signaturesNeeded}</h4>
|
||||
${tableHtml}
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error("Error fetching minter admins:", err)
|
||||
adminListContainer.innerHTML =
|
||||
@ -714,6 +764,36 @@ const handleRemoveMinterGroupAdmin = async (name, address) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteARCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||
if (!confirmed) return
|
||||
const blankData = {
|
||||
header: "",
|
||||
content: "",
|
||||
links: [],
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: ""
|
||||
}
|
||||
let base64Data = await objectToBase64(blankData)
|
||||
if (!base64Data) {
|
||||
base64Data = btoa(JSON.stringify(blankData))
|
||||
}
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64Data,
|
||||
})
|
||||
alert("Your card has been effectively deleted.")
|
||||
} catch (error) {
|
||||
console.error("Error deleting AR card:", error)
|
||||
alert("Failed to delete the card. Check console for details.")
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => {
|
||||
// Ensure we have addresses
|
||||
if (!minterGroupMembers) {
|
||||
@ -920,6 +1000,16 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
${creator === userState.accountName ? `
|
||||
<div style="margin-top: 0.8em;">
|
||||
<button
|
||||
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
|
||||
onclick="deleteARCard('${cardIdentifier}')"
|
||||
>
|
||||
DELETE CARD
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
|
@ -439,123 +439,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
selectedSort = sortSelect.value
|
||||
}
|
||||
|
||||
if (selectedSort === 'name') {
|
||||
// Sort alphabetically by the minter's name
|
||||
finalCards.sort((a, b) => {
|
||||
const nameA = a.decryptedCardData.minterName?.toLowerCase() || ''
|
||||
const nameB = b.decryptedCardData.minterName?.toLowerCase() || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
} else if (selectedSort === 'recent-comments') {
|
||||
// We need each card's newest comment timestamp for sorting
|
||||
for (let card of finalCards) {
|
||||
card.newestCommentTimestamp = await getNewestAdminCommentTimestamp(card.card.identifier)
|
||||
}
|
||||
// Then sort descending by newest comment
|
||||
finalCards.sort((a, b) =>
|
||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
)
|
||||
} else if (selectedSort === 'least-votes') {
|
||||
// TODO: Add the logic to sort by LEAST total ADMIN votes, then totalYesWeight
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const finalCard of finalCards) {
|
||||
try {
|
||||
const pollName = finalCard.decryptedCardData.poll
|
||||
// If card or poll is missing, default to zero
|
||||
if (!pollName) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults || pollResults.error) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
// Pull only the adminYes/adminNo/totalYesWeight from processPollData
|
||||
const {
|
||||
adminYes,
|
||||
adminNo,
|
||||
totalYesWeight
|
||||
} = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
finalCard.decryptedCardData.creator,
|
||||
finalCard.card.identifier
|
||||
)
|
||||
finalCard._adminTotalVotes = adminYes + adminNo
|
||||
finalCard._yesWeight = totalYesWeight
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
}
|
||||
}
|
||||
// Sort ascending by (adminYes + adminNo), then descending by totalYesWeight
|
||||
finalCards.sort((a, b) => {
|
||||
const diffAdminTotal = a._adminTotalVotes - b._adminTotalVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
// If there's a tie, show the card with higher yesWeight first
|
||||
return b._yesWeight - a._yesWeight
|
||||
})
|
||||
} else if (selectedSort === 'most-votes') {
|
||||
// TODO: Add the logic to sort by MOST total ADMIN votes, then totalYesWeight
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const finalCard of finalCards) {
|
||||
try {
|
||||
const pollName = finalCard.decryptedCardData.poll
|
||||
if (!pollName) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults || pollResults.error) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const {
|
||||
adminYes,
|
||||
adminNo,
|
||||
totalYesWeight
|
||||
} = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
finalCard.decryptedCardData.creator,
|
||||
finalCard.card.identifier
|
||||
)
|
||||
finalCard._adminTotalVotes = adminYes + adminNo
|
||||
finalCard._yesWeight = totalYesWeight
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
}
|
||||
}
|
||||
// Sort descending by (adminYes + adminNo), then descending by totalYesWeight
|
||||
finalCards.sort((a, b) => {
|
||||
const diffAdminTotal = b._adminTotalVotes - a._adminTotalVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
return b._yesWeight - a._yesWeight
|
||||
})
|
||||
} else {
|
||||
// Sort cards by timestamp (most recent first)
|
||||
finalCards.sort((a, b) => {
|
||||
const timestampA = a.card.updated || a.card.created || 0
|
||||
const timestampB = b.card.updated || b.card.created || 0
|
||||
return timestampB - timestampA;
|
||||
})
|
||||
}
|
||||
const sortedFinalCards = await sortCards(finalCards, selectedSort, "admin")
|
||||
|
||||
encryptedCardsContainer.innerHTML = ""
|
||||
|
||||
const finalVisualFilterCards = finalCards.filter(({card}) => {
|
||||
const finalVisualFilterCards = sortedFinalCards.filter(({card}) => {
|
||||
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
||||
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
|
||||
|
||||
@ -1257,20 +1145,36 @@ const handleBanMinter = async (minterName) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
|
||||
const deleteAdminCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const comments = await fetchEncryptedComments(cardIdentifier)
|
||||
if (!comments || comments.length === 0) {
|
||||
return 0
|
||||
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||
if (!confirmed) return
|
||||
const blankData = {
|
||||
header: "",
|
||||
content: "",
|
||||
links: [],
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: ""
|
||||
}
|
||||
const newestTimestamp = comments.reduce((acc, comment) => {
|
||||
const cTime = comment.updated || comment.created || 0
|
||||
return cTime > acc ? cTime : acc
|
||||
}, 0)
|
||||
return newestTimestamp
|
||||
} catch (err) {
|
||||
console.error('Failed to get newest comment timestamp:', err)
|
||||
return 0
|
||||
let base64Data = await objectToBase64(blankData)
|
||||
if (!base64Data) {
|
||||
base64Data = btoa(JSON.stringify(blankData))
|
||||
}
|
||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "MAIL_PRIVATE",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64Data,
|
||||
encrypt: true,
|
||||
publicKeys: verifiedAdminPublicKeys
|
||||
})
|
||||
alert("Your card has been effectively deleted.")
|
||||
} catch (error) {
|
||||
console.error("Error deleting Admin card:", error)
|
||||
alert("Failed to delete the card. Check console for details.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1352,9 +1256,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
showRemoveHtml = removeActionsHtml
|
||||
|
||||
if (userVote === 0) {
|
||||
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
|
||||
cardColorCode = "rgba(1, 128, 20, 0.35)"; // or any green you want
|
||||
} else if (userVote === 1) {
|
||||
cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want
|
||||
cardColorCode = "rgba(124, 6, 6, 0.45)"; // or any red you want
|
||||
}
|
||||
|
||||
const confirmedKick = finalKickTxs.some(
|
||||
@ -1475,6 +1379,16 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
${creator === userState.accountName ? `
|
||||
<div style="margin-top: 0.8em;">
|
||||
<button
|
||||
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
|
||||
onclick="deleteAdminCard('${cardIdentifier}')"
|
||||
>
|
||||
DELETE CARD
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
|
@ -366,7 +366,7 @@ const processARBoardCards = async (allValidCards) => {
|
||||
const loadCards = async (cardIdentifierPrefix) => {
|
||||
const cardsContainer = document.getElementById("cards-container")
|
||||
let isARBoard = false
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
||||
cardsContainer.innerHTML = `<p style="color:white;">Loading cards...</p>`
|
||||
|
||||
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
||||
isARBoard = true
|
||||
@ -418,30 +418,13 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
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') {
|
||||
// If you need the newest comment timestamp
|
||||
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') {
|
||||
await applyVoteSortingData(finalCards, /* ascending= */ true)
|
||||
} else if (selectedSort === 'most-votes') {
|
||||
await applyVoteSortingData(finalCards, /* ascending= */ false)
|
||||
}
|
||||
// else 'newest' => do nothing (already sorted newest-first by your process functions).
|
||||
const sortedFinalCards = isARBoard
|
||||
? await sortCards(finalCards, selectedSort, "ar")
|
||||
: await sortCards(finalCards, selectedSort, "minter")
|
||||
// Create the 'finalCardsArray' that includes the data, etc.
|
||||
let finalCardsArray = []
|
||||
cardsContainer.innerHTML = ''
|
||||
for (const card of finalCards) {
|
||||
for (const card of sortedFinalCards) {
|
||||
try {
|
||||
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||
@ -560,71 +543,6 @@ const verifyMinter = async (minterName) => {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -827,236 +745,6 @@ const publishCard = async (cardIdentifierPrefix) => {
|
||||
}
|
||||
}
|
||||
|
||||
let globalVoterMap = new Map()
|
||||
|
||||
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => {
|
||||
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,
|
||||
detailsHtml: `<p>Poll data is invalid or missing.</p>`,
|
||||
userVote: null
|
||||
}
|
||||
}
|
||||
|
||||
const memberAddresses = minterGroupMembers.map(m => m.member)
|
||||
const minterAdminAddresses = minterAdmins.map(m => m.member)
|
||||
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
|
||||
const featureTriggerPassed = await featureTriggerCheck()
|
||||
const groupAdminAddresses = adminGroupsMembers.map(m => m.member)
|
||||
let adminAddresses = [...minterAdminAddresses]
|
||||
|
||||
if (!featureTriggerPassed) {
|
||||
console.log(`featureTrigger is NOT passed, only showing admin results from Minter Admins and Group Admins`)
|
||||
adminAddresses = [...minterAdminAddresses, ...groupAdminAddresses]
|
||||
}
|
||||
|
||||
let adminYes = 0, adminNo = 0
|
||||
let minterYes = 0, minterNo = 0
|
||||
let yesWeight = 0, noWeight = 0
|
||||
let userVote = null
|
||||
|
||||
for (const w of pollData.voteWeights) {
|
||||
if (w.optionName.toLowerCase() === 'yes') {
|
||||
yesWeight = w.voteWeight
|
||||
} else if (w.optionName.toLowerCase() === 'no') {
|
||||
noWeight = w.voteWeight
|
||||
}
|
||||
}
|
||||
|
||||
const voterPromises = pollData.votes.map(async (vote) => {
|
||||
const optionIndex = vote.optionIndex; // 0 => yes, 1 => no
|
||||
const voterPublicKey = vote.voterPublicKey
|
||||
const voterAddress = await getAddressFromPublicKey(voterPublicKey)
|
||||
|
||||
if (voterAddress === userState.accountAddress) {
|
||||
userVote = optionIndex
|
||||
}
|
||||
|
||||
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.`)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
let blocksMinted = 0
|
||||
try {
|
||||
const addressInfo = await getAddressInfo(voterAddress)
|
||||
blocksMinted = addressInfo?.blocksMinted || 0
|
||||
} catch (e) {
|
||||
console.warn(`Failed to get addressInfo for ${voterAddress}`, e)
|
||||
}
|
||||
const isAdmin = adminAddresses.includes(voterAddress)
|
||||
const isMinter = memberAddresses.includes(voterAddress)
|
||||
|
||||
return {
|
||||
optionIndex,
|
||||
voterPublicKey,
|
||||
voterAddress,
|
||||
voterName,
|
||||
isAdmin,
|
||||
isMinter,
|
||||
blocksMinted
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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</h1><h3 style="color: white; text-align: center; font-size: 1.8rem"> Support Poll Result Details</h3>
|
||||
<h4 style="color: green; text-align: center;">Yes Vote Details</h4>
|
||||
${yesTableHtml}
|
||||
<h4 style="color: red; text-align: center; margin-top: 2em;">No Vote Details</h4>
|
||||
${noTableHtml}
|
||||
</div>
|
||||
`
|
||||
const totalYes = adminYes + minterYes
|
||||
const totalNo = adminNo + minterNo
|
||||
|
||||
return {
|
||||
adminYes,
|
||||
adminNo,
|
||||
minterYes,
|
||||
minterNo,
|
||||
totalYes,
|
||||
totalNo,
|
||||
totalYesWeight: totalMinterAndAdminYesWeight,
|
||||
totalNoWeight: totalMinterAndAdminNoWeight,
|
||||
detailsHtml,
|
||||
userVote
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const buildVotersTableHtml = (voters, tableColor) => {
|
||||
if (!voters.length) {
|
||||
return `<p>No voters here.</p>`
|
||||
}
|
||||
|
||||
// Decide extremely dark background for the <tbody>
|
||||
let bodyBackground
|
||||
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.15rem;
|
||||
border-color: #576b6f;
|
||||
margin-bottom: 1em;
|
||||
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</th>
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Type</th>
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Weight(=BlocksMinted)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Tbody with extremely dark green or red -->
|
||||
<tbody style="background-color: ${bodyBackground}; color: #c6c6c6;">
|
||||
${voters
|
||||
.map(v => {
|
||||
const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User"
|
||||
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.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${displayName}</td>
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${userType}</td>
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${v.blocksMinted}</td>
|
||||
</tr>
|
||||
`
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
// Post a comment on a card. ---------------------------------
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
|
||||
@ -1427,20 +1115,6 @@ const createInviteButtonHtml = (creator, cardIdentifier) => {
|
||||
`
|
||||
}
|
||||
|
||||
const featureTriggerCheck = async () => {
|
||||
const latestBlockInfo = await getLatestBlockInfo()
|
||||
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
||||
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) => {
|
||||
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
||||
const isBlockPassed = await featureTriggerCheck()
|
||||
@ -1563,13 +1237,14 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
getNameFromAddress
|
||||
)
|
||||
|
||||
if (transactionType === "GROUP_INVITE" && isSomeTypaAdmin) {
|
||||
if (transactionType === "GROUP_INVITE") {
|
||||
const approvalButtonHtml = `
|
||||
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
||||
<p style="color: rgb(181, 214, 100);">
|
||||
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
||||
</p>
|
||||
${tableHtml}
|
||||
${isSomeTypaAdmin ? `
|
||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button
|
||||
style="
|
||||
@ -1588,6 +1263,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Approve Invite Tx
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
return approvalButtonHtml
|
||||
@ -1653,13 +1329,14 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
return approvalButtonHtml
|
||||
}
|
||||
|
||||
if (transactionType === "ADD_GROUP_ADMIN" && isSomeTypaAdmin) {
|
||||
if (transactionType === "ADD_GROUP_ADMIN") {
|
||||
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}
|
||||
${isSomeTypaAdmin ? `
|
||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button
|
||||
style="
|
||||
@ -1678,18 +1355,20 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Approve Add-Admin Tx
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
return approvalButtonHtml
|
||||
}
|
||||
|
||||
if (transactionType === "REMOVE_GROUP_ADMIN" && isSomeTypaAdmin) {
|
||||
if (transactionType === "REMOVE_GROUP_ADMIN") {
|
||||
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}
|
||||
${isSomeTypaAdmin ? `
|
||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button
|
||||
style="
|
||||
@ -1708,6 +1387,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Approve Remove-Admin Tx
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
return approvalButtonHtml
|
||||
@ -1742,9 +1422,19 @@ const buildApprovalTableHtml = async (approvalTxs, getNameFunc) => {
|
||||
: "(No registered name)"
|
||||
|
||||
const dateStr = new Date(tx.timestamp).toLocaleString()
|
||||
// Check whether this is the current user
|
||||
const isCurrentUser =
|
||||
userState &&
|
||||
userState.accountName &&
|
||||
adminName &&
|
||||
adminName.toLowerCase() === userState.accountName.toLowerCase();
|
||||
// If it's the current user, highlight the row (change to any color/style you prefer)
|
||||
const rowStyle = isCurrentUser
|
||||
? "background: rgba(178, 255, 89, 0.2);" // light green highlight
|
||||
: "";
|
||||
return `
|
||||
<tr>
|
||||
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565">${displayName}</td>
|
||||
<tr style="${rowStyle}">
|
||||
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: dodgerblue">${displayName}</td>
|
||||
<td style="border: 1px solid rgb(255, 254, 254); padding: 4px;">${dateStr}</td>
|
||||
</tr>
|
||||
`
|
||||
@ -1863,23 +1553,33 @@ const getMinterAvatar = async (minterName) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getNewestCommentTimestamp = async (cardIdentifier) => {
|
||||
const deleteCard = 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
|
||||
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||
if (!confirmed) return
|
||||
const blankData = {
|
||||
header: "",
|
||||
content: "",
|
||||
links: [],
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: ""
|
||||
}
|
||||
// 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
|
||||
let base64Data = await objectToBase64(blankData)
|
||||
if (!base64Data) {
|
||||
base64Data = btoa(JSON.stringify(blankData))
|
||||
}
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64Data,
|
||||
})
|
||||
alert("Your card has been effectively deleted.")
|
||||
} catch (error) {
|
||||
console.error("Error deleting Minter card:", error)
|
||||
alert("Failed to delete the card. Check console for details.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1990,6 +1690,16 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
${creator === userState.accountName ? `
|
||||
<div style="margin-top: 0.8em;">
|
||||
<button
|
||||
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
|
||||
onclick="deleteCard('${cardIdentifier}')"
|
||||
>
|
||||
DELETE CARD
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
|
@ -644,6 +644,33 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteMessage = async (room, existingMessageIdentifier) => {
|
||||
try {
|
||||
const blankMessageObject = {
|
||||
messageHtml: "<em>This post has been deleted.</em>",
|
||||
hasAttachment: false,
|
||||
attachments: [],
|
||||
replyTo: null
|
||||
}
|
||||
const base64Message = btoa(JSON.stringify(blankMessageObject))
|
||||
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"
|
||||
const request = {
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: service,
|
||||
identifier: existingMessageIdentifier,
|
||||
data64: base64Message
|
||||
}
|
||||
if (room === "admins") {
|
||||
request.encrypt = true
|
||||
request.publicKeys = adminPublicKeys
|
||||
}
|
||||
console.log("Deleting forum message...")
|
||||
await qortalRequest(request)
|
||||
} catch (err) {
|
||||
console.error("Error deleting message:", err)
|
||||
}
|
||||
}
|
||||
|
||||
function clearInputs() {
|
||||
// Clear the file input elements and preview container
|
||||
@ -760,6 +787,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
}
|
||||
|
||||
handleReplyLogic(fetchMessages)
|
||||
handleDeleteLogic(fetchMessages, room)
|
||||
|
||||
await updatePaginationControls(room, limit)
|
||||
} catch (error) {
|
||||
@ -979,6 +1007,16 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
||||
const replyHtml = await buildReplyHtml(message, room)
|
||||
const attachmentHtml = await buildAttachmentHtml(message, room)
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
|
||||
let deleteButtonHtml = ''
|
||||
if (message.name === userState.accountName) {
|
||||
deleteButtonHtml = `
|
||||
<button class="delete-button"
|
||||
data-message-identifier="${message.identifier}"
|
||||
data-room="${room}">
|
||||
Delete
|
||||
</button>
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
@ -995,7 +1033,10 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
||||
<div class="attachments-gallery">
|
||||
${attachmentHtml}
|
||||
</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
<div class="message-actions">
|
||||
${deleteButtonHtml}
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@ -1147,6 +1188,24 @@ const handleReplyLogic = (fetchMessages) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteLogic = (fetchMessages, room) => {
|
||||
// Only select buttons that do NOT already have a listener
|
||||
const deleteButtons = document.querySelectorAll('.delete-button:not(.bound-delete)')
|
||||
deleteButtons.forEach(button => {
|
||||
button.classList.add('bound-delete')
|
||||
button.addEventListener('click', async () => {
|
||||
const messageId = button.dataset.messageIdentifier
|
||||
const postRoom = button.dataset.room
|
||||
const msg = fetchMessages.find(m => m && m.identifier === messageId)
|
||||
if (msg) {
|
||||
const confirmed = confirm("Are you sure you want to delete this post?")
|
||||
if (!confirmed) return
|
||||
await handleDeleteMessage(postRoom, messageId)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const showReplyPreview = (repliedMessage) => {
|
||||
replyToMessageIdentifier = repliedMessage.identifier
|
||||
|
||||
|
@ -53,7 +53,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${day}.${month}.${year}..@${hours}:${minutes}:${seconds}`
|
||||
const formattedDate = `${year}.${month}.${day} @ ${hours}:${minutes}:${seconds}`
|
||||
console.log('Formatted date:', formattedDate)
|
||||
return formattedDate
|
||||
}
|
||||
|
@ -163,9 +163,9 @@ const fetchAllKickBanTxData = async () => {
|
||||
finalBanTxs,
|
||||
pendingBanTxs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const partitionTransactions = (txSearchResults) => {
|
||||
const partitionTransactions = (txSearchResults) => {
|
||||
const finalTx = []
|
||||
const pendingTx = []
|
||||
|
||||
@ -178,7 +178,418 @@ const fetchAllKickBanTxData = async () => {
|
||||
}
|
||||
|
||||
return { finalTx, pendingTx };
|
||||
}
|
||||
|
||||
const sortCards = async (cardsArray, selectedSort, board) => {
|
||||
// Default sort is by newest if none provided
|
||||
if (!selectedSort) selectedSort = 'newest'
|
||||
switch (selectedSort) {
|
||||
case 'name':
|
||||
// Sort by name
|
||||
cardsArray.sort((a, b) => {
|
||||
const nameA = (board === "admin")
|
||||
? (a.decryptedCardData?.minterName || '').toLowerCase()
|
||||
: ((board === "ar")
|
||||
? (a.minterName?.toLowerCase() || '')
|
||||
: (a.name?.toLowerCase() || '')
|
||||
)
|
||||
const nameB = (board === "admin")
|
||||
? (b.decryptedCardData?.minterName || '').toLowerCase()
|
||||
: ((board === "ar")
|
||||
? (b.minterName?.toLowerCase() || '')
|
||||
: (b.name?.toLowerCase() || '')
|
||||
)
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
break
|
||||
case 'recent-comments':
|
||||
// Sort by newest comment timestamp
|
||||
for (let card of cardsArray) {
|
||||
const cardIdentifier = (board === "admin")
|
||||
? card.card.identifier
|
||||
: card.identifier
|
||||
card.newestCommentTimestamp = await getNewestCommentTimestamp(cardIdentifier, board)
|
||||
}
|
||||
cardsArray.sort((a, b) => {
|
||||
return (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
})
|
||||
break
|
||||
case 'least-votes':
|
||||
await applyVoteSortingData(cardsArray, /* ascending= */ true, board)
|
||||
break
|
||||
case 'most-votes':
|
||||
await applyVoteSortingData(cardsArray, /* ascending= */ false, board)
|
||||
break
|
||||
default:
|
||||
// Sort by date
|
||||
cardsArray.sort((a, b) => {
|
||||
const timestampA = (board === "admin")
|
||||
? a.card.updated || a.card.created || 0
|
||||
: a.updated || a.created || 0
|
||||
const timestampB = (board === "admin")
|
||||
? b.card.updated || b.card.created || 0
|
||||
: b.updated || b.created || 0
|
||||
return timestampB - timestampA;
|
||||
})
|
||||
break
|
||||
}
|
||||
return cardsArray
|
||||
}
|
||||
|
||||
const getNewestCommentTimestamp = async (cardIdentifier, board) => {
|
||||
try {
|
||||
const comments = (board === "admin") ? await fetchEncryptedComments(cardIdentifier) : await fetchCommentsForCard(cardIdentifier)
|
||||
if (!comments || comments.length === 0) {
|
||||
return 0
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const applyVoteSortingData = async (cards, ascending = true, boardType = 'minter') => {
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const card of cards) {
|
||||
try {
|
||||
if (boardType === 'admin') {
|
||||
// For the Admin board, we already have the poll name in `card.decryptedCardData.poll`
|
||||
// No need to fetch the resource from BLOG_POST
|
||||
const pollName = card.decryptedCardData?.poll
|
||||
if (!pollName) {
|
||||
// No poll => no votes
|
||||
card._adminVotes = 0
|
||||
card._adminYes = 0
|
||||
card._minterVotes = 0
|
||||
card._minterYes = 0
|
||||
continue
|
||||
}
|
||||
// Fetch poll results
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults) {
|
||||
card._adminVotes = 0
|
||||
card._adminYes = 0
|
||||
card._minterVotes = 0
|
||||
card._minterYes = 0
|
||||
continue
|
||||
}
|
||||
// Process them
|
||||
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
card.decryptedCardData.creator,
|
||||
card.card.identifier
|
||||
)
|
||||
card._adminVotes = adminYes + adminNo
|
||||
card._adminYes = adminYes
|
||||
card._minterVotes = minterYes + minterNo
|
||||
card._minterYes = minterYes
|
||||
} else {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let globalVoterMap = new Map()
|
||||
|
||||
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => {
|
||||
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,
|
||||
detailsHtml: `<p>Poll data is invalid or missing.</p>`,
|
||||
userVote: null
|
||||
}
|
||||
}
|
||||
|
||||
const memberAddresses = minterGroupMembers.map(m => m.member)
|
||||
const minterAdminAddresses = minterAdmins.map(m => m.member)
|
||||
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
|
||||
const featureTriggerPassed = await featureTriggerCheck()
|
||||
const groupAdminAddresses = adminGroupsMembers.map(m => m.member)
|
||||
let adminAddresses = [...minterAdminAddresses]
|
||||
|
||||
if (!featureTriggerPassed) {
|
||||
console.log(`featureTrigger is NOT passed, only showing admin results from Minter Admins and Group Admins`)
|
||||
adminAddresses = [...minterAdminAddresses, ...groupAdminAddresses]
|
||||
}
|
||||
|
||||
|
||||
|
||||
let adminYes = 0, adminNo = 0
|
||||
let minterYes = 0, minterNo = 0
|
||||
let yesWeight = 0, noWeight = 0
|
||||
let userVote = null
|
||||
|
||||
for (const w of pollData.voteWeights) {
|
||||
if (w.optionName.toLowerCase() === 'yes') {
|
||||
yesWeight = w.voteWeight
|
||||
} else if (w.optionName.toLowerCase() === 'no') {
|
||||
noWeight = w.voteWeight
|
||||
}
|
||||
}
|
||||
|
||||
const voterPromises = pollData.votes.map(async (vote) => {
|
||||
const optionIndex = vote.optionIndex; // 0 => yes, 1 => no
|
||||
const voterPublicKey = vote.voterPublicKey
|
||||
const voterAddress = await getAddressFromPublicKey(voterPublicKey)
|
||||
|
||||
if (voterAddress === userState.accountAddress) {
|
||||
userVote = optionIndex
|
||||
}
|
||||
|
||||
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.`)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
let blocksMinted = 0
|
||||
try {
|
||||
const addressInfo = await getAddressInfo(voterAddress)
|
||||
blocksMinted = addressInfo?.blocksMinted || 0
|
||||
} catch (e) {
|
||||
console.warn(`Failed to get addressInfo for ${voterAddress}`, e)
|
||||
}
|
||||
const isAdmin = adminAddresses.includes(voterAddress)
|
||||
const isMinter = memberAddresses.includes(voterAddress)
|
||||
|
||||
return {
|
||||
optionIndex,
|
||||
voterPublicKey,
|
||||
voterAddress,
|
||||
voterName,
|
||||
isAdmin,
|
||||
isMinter,
|
||||
blocksMinted
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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</h1><h3 style="color: white; text-align: center; font-size: 1.8rem"> Support Poll Result Details</h3>
|
||||
<h4 style="color: green; text-align: center;">Yes Vote Details</h4>
|
||||
${yesTableHtml}
|
||||
<h4 style="color: red; text-align: center; margin-top: 2em;">No Vote Details</h4>
|
||||
${noTableHtml}
|
||||
</div>
|
||||
`
|
||||
const totalYes = adminYes + minterYes
|
||||
const totalNo = adminNo + minterNo
|
||||
|
||||
return {
|
||||
adminYes,
|
||||
adminNo,
|
||||
minterYes,
|
||||
minterNo,
|
||||
totalYes,
|
||||
totalNo,
|
||||
totalYesWeight: totalMinterAndAdminYesWeight,
|
||||
totalNoWeight: totalMinterAndAdminNoWeight,
|
||||
detailsHtml,
|
||||
userVote
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const buildVotersTableHtml = (voters, tableColor) => {
|
||||
if (!voters.length) {
|
||||
return `<p>No voters here.</p>`
|
||||
}
|
||||
|
||||
// Decide extremely dark background for the <tbody>
|
||||
let bodyBackground
|
||||
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.15rem;
|
||||
border-color: #576b6f;
|
||||
margin-bottom: 1em;
|
||||
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</th>
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Type</th>
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Weight(=BlocksMinted)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Tbody with extremely dark green or red -->
|
||||
<tbody style="background-color: ${bodyBackground}; color: #c6c6c6;">
|
||||
${voters
|
||||
.map(v => {
|
||||
const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User"
|
||||
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.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${displayName}</td>
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${userType}</td>
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${v.blocksMinted}</td>
|
||||
</tr>
|
||||
`
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
const featureTriggerCheck = async () => {
|
||||
const latestBlockInfo = await getLatestBlockInfo()
|
||||
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -32,11 +32,7 @@
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Variable-based versioning (credit: QuickMythril)
|
||||
; // Update here in the future
|
||||
</script>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
|
||||
|
||||
<nav class="navbar navbar-dropdown navbar-expand-lg">
|
||||
|
Loading…
Reference in New Issue
Block a user