QM-Mintership v1.05.2[2]b #8
@ -163,6 +163,21 @@
|
|||||||
background-color: #19403d;
|
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 */
|
/* forum-styles.css additions */
|
||||||
|
|
||||||
.message-input-section {
|
.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;">
|
<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>
|
<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>
|
<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;">
|
<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="0">Show All</option>
|
||||||
<option value="1">Last 1 day</option>
|
<option value="1">Last 1 day</option>
|
||||||
@ -121,9 +128,15 @@ const loadAddRemoveAdminPage = async () => {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
await publishARCard(addRemoveIdentifierPrefix)
|
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 featureTriggerCheck()
|
||||||
await loadCards(addRemoveIdentifierPrefix)
|
|
||||||
await displayExistingMinterAdmins()
|
await displayExistingMinterAdmins()
|
||||||
|
await loadCards(addRemoveIdentifierPrefix)
|
||||||
await fetchAllARTxData()
|
await fetchAllARTxData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +146,19 @@ const toggleProposeButton = () => {
|
|||||||
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
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 fetchAllARTxData = async () => {
|
||||||
const addAdmTx = "ADD_GROUP_ADMIN"
|
const addAdmTx = "ADD_GROUP_ADMIN"
|
||||||
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
||||||
@ -215,7 +241,29 @@ const displayExistingMinterAdmins = async () => {
|
|||||||
try {
|
try {
|
||||||
// 1) Fetch addresses
|
// 1) Fetch addresses
|
||||||
const admins = await fetchMinterGroupAdmins()
|
const admins = await fetchMinterGroupAdmins()
|
||||||
|
// Get names for each admin first
|
||||||
|
for (const admin of admins) {
|
||||||
|
try {
|
||||||
|
admin.name = await getNameFromAddress(admin.member)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Error fetching name for ${admin.member}:`, err)
|
||||||
|
admin.name = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort admin list by name with NULL ACCOUNT at the top
|
||||||
|
admins.sort((a, b) => {
|
||||||
|
if (a.member === nullAddress && b.member !== nullAddress) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.member === nullAddress && a.member !== nullAddress) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return (a.name || '').localeCompare(b.name || '');
|
||||||
|
})
|
||||||
minterAdminAddresses = admins.map(m => m.member)
|
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 = "";
|
let rowsHtml = "";
|
||||||
for (const adminAddr of admins) {
|
for (const adminAddr of admins) {
|
||||||
if (adminAddr.member === nullAddress) {
|
if (adminAddr.member === nullAddress) {
|
||||||
@ -235,16 +283,8 @@ const displayExistingMinterAdmins = async () => {
|
|||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
// Attempt to get name
|
|
||||||
let adminName
|
|
||||||
try {
|
|
||||||
adminName = await getNameFromAddress(adminAddr.member)
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`Error fetching name for ${adminAddr.member}:`, err)
|
|
||||||
adminName = null
|
|
||||||
}
|
}
|
||||||
const displayName = adminName && adminName !== adminAddr.member ? adminName : "(No Name)"
|
const displayName = adminAddr.name && adminAddr.name !== adminAddr.member ? adminAddr.name : "(No Name)"
|
||||||
rowsHtml += `
|
rowsHtml += `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td>
|
<td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td>
|
||||||
@ -252,7 +292,7 @@ const displayExistingMinterAdmins = async () => {
|
|||||||
<td style="border: 1px solid rgb(231, 112, 112); padding: 4px;">
|
<td style="border: 1px solid rgb(231, 112, 112); padding: 4px;">
|
||||||
<button
|
<button
|
||||||
style="padding: 5px; background: red; color: white; border-radius: 3px; cursor: pointer;"
|
style="padding: 5px; background: red; color: white; border-radius: 3px; cursor: pointer;"
|
||||||
onclick="handleProposeDemotionWrapper('${adminName}', '${adminAddr.member}')"
|
onclick="handleProposeDemotionWrapper('${adminAddr.name}', '${adminAddr.member}')"
|
||||||
>
|
>
|
||||||
Propose Demotion
|
Propose Demotion
|
||||||
</button>
|
</button>
|
||||||
@ -262,6 +302,22 @@ const displayExistingMinterAdmins = async () => {
|
|||||||
}
|
}
|
||||||
// 3) Build the table
|
// 3) Build the table
|
||||||
const tableHtml = `
|
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;">
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
|
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
|
||||||
@ -274,8 +330,13 @@ const displayExistingMinterAdmins = async () => {
|
|||||||
${rowsHtml}
|
${rowsHtml}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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) {
|
} catch (err) {
|
||||||
console.error("Error fetching minter admins:", err)
|
console.error("Error fetching minter admins:", err)
|
||||||
adminListContainer.innerHTML =
|
adminListContainer.innerHTML =
|
||||||
@ -714,6 +775,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) => {
|
const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => {
|
||||||
// Ensure we have addresses
|
// Ensure we have addresses
|
||||||
if (!minterGroupMembers) {
|
if (!minterGroupMembers) {
|
||||||
@ -920,6 +1011,16 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
|||||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||||
</div>
|
</div>
|
||||||
</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-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
<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>
|
<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
|
selectedSort = sortSelect.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSort === 'name') {
|
const sortedFinalCards = await sortCards(finalCards, selectedSort, "admin")
|
||||||
// Sort alphabetically by the minter's name
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
const nameA = a.decryptedCardData.minterName?.toLowerCase() || ''
|
|
||||||
const nameB = b.decryptedCardData.minterName?.toLowerCase() || ''
|
|
||||||
return nameA.localeCompare(nameB)
|
|
||||||
})
|
|
||||||
} else if (selectedSort === 'recent-comments') {
|
|
||||||
// We need each card's newest comment timestamp for sorting
|
|
||||||
for (let card of finalCards) {
|
|
||||||
card.newestCommentTimestamp = await getNewestAdminCommentTimestamp(card.card.identifier)
|
|
||||||
}
|
|
||||||
// Then sort descending by newest comment
|
|
||||||
finalCards.sort((a, b) =>
|
|
||||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
|
||||||
)
|
|
||||||
} else if (selectedSort === 'least-votes') {
|
|
||||||
// TODO: Add the logic to sort by LEAST total ADMIN votes, then totalYesWeight
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
|
||||||
for (const finalCard of finalCards) {
|
|
||||||
try {
|
|
||||||
const pollName = finalCard.decryptedCardData.poll
|
|
||||||
// If card or poll is missing, default to zero
|
|
||||||
if (!pollName) {
|
|
||||||
finalCard._adminTotalVotes = 0
|
|
||||||
finalCard._yesWeight = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const pollResults = await fetchPollResults(pollName)
|
|
||||||
if (!pollResults || pollResults.error) {
|
|
||||||
finalCard._adminTotalVotes = 0
|
|
||||||
finalCard._yesWeight = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Pull only the adminYes/adminNo/totalYesWeight from processPollData
|
|
||||||
const {
|
|
||||||
adminYes,
|
|
||||||
adminNo,
|
|
||||||
totalYesWeight
|
|
||||||
} = await processPollData(
|
|
||||||
pollResults,
|
|
||||||
minterGroupMembers,
|
|
||||||
minterAdmins,
|
|
||||||
finalCard.decryptedCardData.creator,
|
|
||||||
finalCard.card.identifier
|
|
||||||
)
|
|
||||||
finalCard._adminTotalVotes = adminYes + adminNo
|
|
||||||
finalCard._yesWeight = totalYesWeight
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
|
||||||
finalCard._adminTotalVotes = 0
|
|
||||||
finalCard._yesWeight = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort ascending by (adminYes + adminNo), then descending by totalYesWeight
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
const diffAdminTotal = a._adminTotalVotes - b._adminTotalVotes
|
|
||||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
|
||||||
// If there's a tie, show the card with higher yesWeight first
|
|
||||||
return b._yesWeight - a._yesWeight
|
|
||||||
})
|
|
||||||
} else if (selectedSort === 'most-votes') {
|
|
||||||
// TODO: Add the logic to sort by MOST total ADMIN votes, then totalYesWeight
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
|
||||||
for (const finalCard of finalCards) {
|
|
||||||
try {
|
|
||||||
const pollName = finalCard.decryptedCardData.poll
|
|
||||||
if (!pollName) {
|
|
||||||
finalCard._adminTotalVotes = 0
|
|
||||||
finalCard._yesWeight = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const pollResults = await fetchPollResults(pollName)
|
|
||||||
if (!pollResults || pollResults.error) {
|
|
||||||
finalCard._adminTotalVotes = 0
|
|
||||||
finalCard._yesWeight = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
adminYes,
|
|
||||||
adminNo,
|
|
||||||
totalYesWeight
|
|
||||||
} = await processPollData(
|
|
||||||
pollResults,
|
|
||||||
minterGroupMembers,
|
|
||||||
minterAdmins,
|
|
||||||
finalCard.decryptedCardData.creator,
|
|
||||||
finalCard.card.identifier
|
|
||||||
)
|
|
||||||
finalCard._adminTotalVotes = adminYes + adminNo
|
|
||||||
finalCard._yesWeight = totalYesWeight
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
|
||||||
finalCard._adminTotalVotes = 0
|
|
||||||
finalCard._yesWeight = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort descending by (adminYes + adminNo), then descending by totalYesWeight
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
const diffAdminTotal = b._adminTotalVotes - a._adminTotalVotes
|
|
||||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
|
||||||
return b._yesWeight - a._yesWeight
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Sort cards by timestamp (most recent first)
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
const timestampA = a.card.updated || a.card.created || 0
|
|
||||||
const timestampB = b.card.updated || b.card.created || 0
|
|
||||||
return timestampB - timestampA;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptedCardsContainer.innerHTML = ""
|
encryptedCardsContainer.innerHTML = ""
|
||||||
|
|
||||||
const finalVisualFilterCards = finalCards.filter(({card}) => {
|
const finalVisualFilterCards = sortedFinalCards.filter(({card}) => {
|
||||||
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
||||||
const showHiddenAdminCards = document.getElementById('admin-show-hidden-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 {
|
try {
|
||||||
const comments = await fetchEncryptedComments(cardIdentifier)
|
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||||
if (!comments || comments.length === 0) {
|
if (!confirmed) return
|
||||||
return 0
|
const blankData = {
|
||||||
|
header: "",
|
||||||
|
content: "",
|
||||||
|
links: [],
|
||||||
|
creator: userState.accountName,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
poll: ""
|
||||||
}
|
}
|
||||||
const newestTimestamp = comments.reduce((acc, comment) => {
|
let base64Data = await objectToBase64(blankData)
|
||||||
const cTime = comment.updated || comment.created || 0
|
if (!base64Data) {
|
||||||
return cTime > acc ? cTime : acc
|
base64Data = btoa(JSON.stringify(blankData))
|
||||||
}, 0)
|
}
|
||||||
return newestTimestamp
|
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||||
} catch (err) {
|
await qortalRequest({
|
||||||
console.error('Failed to get newest comment timestamp:', err)
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
return 0
|
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
|
showRemoveHtml = removeActionsHtml
|
||||||
|
|
||||||
if (userVote === 0) {
|
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) {
|
} 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(
|
const confirmedKick = finalKickTxs.some(
|
||||||
@ -1475,6 +1379,16 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
|||||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||||
</div>
|
</div>
|
||||||
</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-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
<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>
|
<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 loadCards = async (cardIdentifierPrefix) => {
|
||||||
const cardsContainer = document.getElementById("cards-container")
|
const cardsContainer = document.getElementById("cards-container")
|
||||||
let isARBoard = false
|
let isARBoard = false
|
||||||
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
cardsContainer.innerHTML = `<p style="color:white;">Loading cards...</p>`
|
||||||
|
|
||||||
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
||||||
isARBoard = true
|
isARBoard = true
|
||||||
@ -418,30 +418,13 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
selectedSort = sortSelect.value
|
selectedSort = sortSelect.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSort === 'name') {
|
const sortedFinalCards = isARBoard
|
||||||
finalCards.sort((a, b) => {
|
? await sortCards(finalCards, selectedSort, "ar")
|
||||||
const nameA = a.name?.toLowerCase() || ''
|
: await sortCards(finalCards, selectedSort, "minter")
|
||||||
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).
|
|
||||||
// Create the 'finalCardsArray' that includes the data, etc.
|
// Create the 'finalCardsArray' that includes the data, etc.
|
||||||
let finalCardsArray = []
|
let finalCardsArray = []
|
||||||
cardsContainer.innerHTML = ''
|
cardsContainer.innerHTML = ''
|
||||||
for (const card of finalCards) {
|
for (const card of sortedFinalCards) {
|
||||||
try {
|
try {
|
||||||
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
||||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
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 removeSkeleton = (cardIdentifier) => {
|
||||||
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
|
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
|
||||||
if (skeletonCard) {
|
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. ---------------------------------
|
// Post a comment on a card. ---------------------------------
|
||||||
const postComment = async (cardIdentifier) => {
|
const postComment = async (cardIdentifier) => {
|
||||||
const commentInput = document.getElementById(`new-comment-${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 checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
|
||||||
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
||||||
const isBlockPassed = await featureTriggerCheck()
|
const isBlockPassed = await featureTriggerCheck()
|
||||||
@ -1563,13 +1237,14 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
getNameFromAddress
|
getNameFromAddress
|
||||||
)
|
)
|
||||||
|
|
||||||
if (transactionType === "GROUP_INVITE" && isSomeTypaAdmin) {
|
if (transactionType === "GROUP_INVITE") {
|
||||||
const approvalButtonHtml = `
|
const approvalButtonHtml = `
|
||||||
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
||||||
<p style="color: rgb(181, 214, 100);">
|
<p style="color: rgb(181, 214, 100);">
|
||||||
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
||||||
</p>
|
</p>
|
||||||
${tableHtml}
|
${tableHtml}
|
||||||
|
${isSomeTypaAdmin ? `
|
||||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||||
<button
|
<button
|
||||||
style="
|
style="
|
||||||
@ -1588,6 +1263,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
Approve Invite Tx
|
Approve Invite Tx
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
return approvalButtonHtml
|
return approvalButtonHtml
|
||||||
@ -1653,13 +1329,14 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
return approvalButtonHtml
|
return approvalButtonHtml
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionType === "ADD_GROUP_ADMIN" && isSomeTypaAdmin) {
|
if (transactionType === "ADD_GROUP_ADMIN") {
|
||||||
const approvalButtonHtml = `
|
const approvalButtonHtml = `
|
||||||
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
||||||
<p style="color: rgb(40, 144, 189);">
|
<p style="color: rgb(40, 144, 189);">
|
||||||
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
||||||
</p>
|
</p>
|
||||||
${tableHtml}
|
${tableHtml}
|
||||||
|
${isSomeTypaAdmin ? `
|
||||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||||
<button
|
<button
|
||||||
style="
|
style="
|
||||||
@ -1678,18 +1355,20 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
Approve Add-Admin Tx
|
Approve Add-Admin Tx
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
return approvalButtonHtml
|
return approvalButtonHtml
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionType === "REMOVE_GROUP_ADMIN" && isSomeTypaAdmin) {
|
if (transactionType === "REMOVE_GROUP_ADMIN") {
|
||||||
const approvalButtonHtml = `
|
const approvalButtonHtml = `
|
||||||
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
<div style="display: flex; flex-direction: column; margin-top: 1em;">
|
||||||
<p style="color: rgb(189, 40, 40);">
|
<p style="color: rgb(189, 40, 40);">
|
||||||
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
||||||
</p>
|
</p>
|
||||||
${tableHtml}
|
${tableHtml}
|
||||||
|
${isSomeTypaAdmin ? `
|
||||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||||
<button
|
<button
|
||||||
style="
|
style="
|
||||||
@ -1708,6 +1387,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
Approve Remove-Admin Tx
|
Approve Remove-Admin Tx
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
return approvalButtonHtml
|
return approvalButtonHtml
|
||||||
@ -1742,9 +1422,19 @@ const buildApprovalTableHtml = async (approvalTxs, getNameFunc) => {
|
|||||||
: "(No registered name)"
|
: "(No registered name)"
|
||||||
|
|
||||||
const dateStr = new Date(tx.timestamp).toLocaleString()
|
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 `
|
return `
|
||||||
<tr>
|
<tr style="${rowStyle}">
|
||||||
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565">${displayName}</td>
|
<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>
|
<td style="border: 1px solid rgb(255, 254, 254); padding: 4px;">${dateStr}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
@ -1863,23 +1553,33 @@ const getMinterAvatar = async (minterName) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNewestCommentTimestamp = async (cardIdentifier) => {
|
const deleteCard = async (cardIdentifier) => {
|
||||||
try {
|
try {
|
||||||
// fetchCommentsForCard returns resources each with at least 'created' or 'updated'
|
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||||
const comments = await fetchCommentsForCard(cardIdentifier)
|
if (!confirmed) return
|
||||||
if (!comments || comments.length === 0) {
|
const blankData = {
|
||||||
// No comments => fallback to 0 (or card's own date, if you like)
|
header: "",
|
||||||
return 0
|
content: "",
|
||||||
|
links: [],
|
||||||
|
creator: userState.accountName,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
poll: ""
|
||||||
}
|
}
|
||||||
// The newest can be determined by comparing 'updated' or 'created'
|
let base64Data = await objectToBase64(blankData)
|
||||||
const newestTimestamp = comments.reduce((acc, c) => {
|
if (!base64Data) {
|
||||||
const cTime = c.updated || c.created || 0
|
base64Data = btoa(JSON.stringify(blankData))
|
||||||
return (cTime > acc) ? cTime : acc
|
}
|
||||||
}, 0)
|
await qortalRequest({
|
||||||
return newestTimestamp
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
} catch (err) {
|
name: userState.accountName,
|
||||||
console.error('Failed to get newest comment timestamp:', err)
|
service: "BLOG_POST",
|
||||||
return 0
|
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>
|
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||||
</div>
|
</div>
|
||||||
</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-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
<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>
|
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||||
|
@ -78,13 +78,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
mintershipForumLinks.forEach(link => {
|
mintershipForumLinks.forEach(link => {
|
||||||
link.addEventListener('click', async (event) => {
|
link.addEventListener('click', async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!userState.isLoggedIn) {
|
|
||||||
await login()
|
|
||||||
}
|
|
||||||
await loadForumPage();
|
await loadForumPage();
|
||||||
loadRoomContent("general")
|
loadRoomContent("general")
|
||||||
startPollingForNewMessages()
|
startPollingForNewMessages()
|
||||||
createScrollToTopButton()
|
createScrollToTopButton()
|
||||||
|
if (!userState.isLoggedIn) {
|
||||||
|
await login()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -92,14 +92,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
minterBoardLinks.forEach(link => {
|
minterBoardLinks.forEach(link => {
|
||||||
link.addEventListener("click", async (event) => {
|
link.addEventListener("click", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!userState.isLoggedIn) {
|
|
||||||
await login()
|
|
||||||
}
|
|
||||||
if (typeof loadMinterBoardPage === "undefined") {
|
if (typeof loadMinterBoardPage === "undefined") {
|
||||||
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
||||||
await loadScript("./assets/js/MinterBoard.js")
|
await loadScript("./assets/js/MinterBoard.js")
|
||||||
}
|
}
|
||||||
await loadMinterBoardPage()
|
await loadMinterBoardPage()
|
||||||
|
if (!userState.isLoggedIn) {
|
||||||
|
await login()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -107,15 +107,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
addRemoveAdminLinks.forEach(link => {
|
addRemoveAdminLinks.forEach(link => {
|
||||||
link.addEventListener('click', async (event) => {
|
link.addEventListener('click', async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// Possibly require user to login if not logged
|
|
||||||
if (!userState.isLoggedIn) {
|
|
||||||
await login()
|
|
||||||
}
|
|
||||||
if (typeof loadMinterBoardPage === "undefined") {
|
if (typeof loadMinterBoardPage === "undefined") {
|
||||||
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
||||||
await loadScript("./assets/js/MinterBoard.js")
|
await loadScript("./assets/js/MinterBoard.js")
|
||||||
}
|
}
|
||||||
await loadAddRemoveAdminPage()
|
await loadAddRemoveAdminPage()
|
||||||
|
if (!userState.isLoggedIn) {
|
||||||
|
await login()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -240,7 +239,10 @@ const loadForumPage = async () => {
|
|||||||
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
|
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;">
|
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;">
|
||||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
${userState.isLoggedIn ? `
|
||||||
|
<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>
|
<span>${userState.accountName || 'Guest'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -644,6 +646,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() {
|
function clearInputs() {
|
||||||
// Clear the file input elements and preview container
|
// Clear the file input elements and preview container
|
||||||
@ -760,6 +789,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleReplyLogic(fetchMessages)
|
handleReplyLogic(fetchMessages)
|
||||||
|
handleDeleteLogic(fetchMessages, room)
|
||||||
|
|
||||||
await updatePaginationControls(room, limit)
|
await updatePaginationControls(room, limit)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -979,6 +1009,16 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
|||||||
const replyHtml = await buildReplyHtml(message, room)
|
const replyHtml = await buildReplyHtml(message, room)
|
||||||
const attachmentHtml = await buildAttachmentHtml(message, room)
|
const attachmentHtml = await buildAttachmentHtml(message, room)
|
||||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
|
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 `
|
return `
|
||||||
<div class="message-item" data-identifier="${message.identifier}">
|
<div class="message-item" data-identifier="${message.identifier}">
|
||||||
@ -995,7 +1035,10 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
|||||||
<div class="attachments-gallery">
|
<div class="attachments-gallery">
|
||||||
${attachmentHtml}
|
${attachmentHtml}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -1147,6 +1190,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) => {
|
const showReplyPreview = (repliedMessage) => {
|
||||||
replyToMessageIdentifier = repliedMessage.identifier
|
replyToMessageIdentifier = repliedMessage.identifier
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
|
|||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
const seconds = String(date.getSeconds()).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)
|
console.log('Formatted date:', formattedDate)
|
||||||
return formattedDate
|
return formattedDate
|
||||||
}
|
}
|
||||||
@ -782,7 +782,7 @@ 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.
|
// 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, after=0) => {
|
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true, after=0) => {
|
||||||
try {
|
try {
|
||||||
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&fter=${after}`
|
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&after=${after}`
|
||||||
|
|
||||||
if (name && !identifier && !room) {
|
if (name && !identifier && !room) {
|
||||||
console.log('name only searchSimple', name)
|
console.log('name only searchSimple', name)
|
||||||
|
@ -163,9 +163,9 @@ const fetchAllKickBanTxData = async () => {
|
|||||||
finalBanTxs,
|
finalBanTxs,
|
||||||
pendingBanTxs,
|
pendingBanTxs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const partitionTransactions = (txSearchResults) => {
|
const partitionTransactions = (txSearchResults) => {
|
||||||
const finalTx = []
|
const finalTx = []
|
||||||
const pendingTx = []
|
const pendingTx = []
|
||||||
|
|
||||||
@ -178,7 +178,418 @@ const fetchAllKickBanTxData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { finalTx, pendingTx };
|
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>
|
</head>
|
||||||
<body>
|
<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">
|
<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">
|
<nav class="navbar navbar-dropdown navbar-expand-lg">
|
||||||
|
Loading…
Reference in New Issue
Block a user