Compare commits

...

10 Commits

Author SHA1 Message Date
QuickMythril
1aa4985375 sort votes by most Yes & least No 2025-02-03 01:46:31 -05:00
QuickMythril
7cfd0357b5 fix blocklist toggle button text 2025-02-03 01:21:17 -05:00
QuickMythril
41e1369d86 prevent cards from automatically loading 2025-02-03 01:15:18 -05:00
QuickMythril
9f645f5582 add message editing to all forums 2025-02-03 01:04:19 -05:00
QuickMythril
3a083f99f6 sort MAM board admin list by name with NULL first 2025-02-03 00:51:49 -05:00
QuickMythril
1b5e8c38e1 load admins before cards in MAM board 2025-02-03 00:48:59 -05:00
QuickMythril
e79e0bf4b1 show public boards when not logged in 2025-02-03 00:48:15 -05:00
QuickMythril
02868171e3 fix applyVoteSortingData for admin board 2025-02-03 00:45:37 -05:00
QuickMythril
9971c6d595 move more code to Shared.js 2025-02-03 00:43:21 -05:00
QuickMythril
0df227d63d move duplicate sorting code & related functions to Shared.js 2025-02-03 00:39:38 -05:00
7 changed files with 542 additions and 532 deletions

View File

@ -178,6 +178,21 @@
background-color: #3d1919;
}
.edit-button {
align-self: flex-end;
margin-top: 1vh;
background-color: #897016;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 0.3vh 0.6vh;
cursor: pointer;
}
.edit-button:hover {
background-color: #402d09;
}
/* forum-styles.css additions */
.message-input-section {

View File

@ -124,26 +124,18 @@ const loadAddRemoveAdminPage = async () => {
linksContainer.appendChild(newLinkInput)
})
const timeRangeSelectCheckbox = document.getElementById('time-range-select')
if (timeRangeSelectCheckbox) {
timeRangeSelectCheckbox.addEventListener('change', async (event) => {
await loadCards(addRemoveIdentifierPrefix)
})
}
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
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)
// Only re-load the cards whenever user presses the refresh button.
})
await featureTriggerCheck()
await loadCards(addRemoveIdentifierPrefix)
await displayExistingMinterAdmins()
// Only load the cards whenever user presses the refresh button.
await fetchAllARTxData()
}
@ -248,6 +240,25 @@ const displayExistingMinterAdmins = async () => {
try {
// 1) Fetch addresses
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)
// Compute total admin count and signatures needed (40%, rounded up)
const totalAdmins = admins.length;
@ -271,16 +282,8 @@ const displayExistingMinterAdmins = async () => {
</tr>
`
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 += `
<tr>
<td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td>
@ -288,7 +291,7 @@ const displayExistingMinterAdmins = async () => {
<td style="border: 1px solid rgb(231, 112, 112); padding: 4px;">
<button
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
</button>

View File

@ -380,123 +380,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
@ -1196,23 +1084,6 @@ const handleBanMinter = async (minterName) => {
}
}
const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
try {
const comments = await fetchEncryptedComments(cardIdentifier)
if (!comments || comments.length === 0) {
return 0
}
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
}
}
const deleteAdminCard = async (cardIdentifier) => {
try {
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")

View File

@ -32,7 +32,7 @@ const loadMinterAdminToolsPage = async () => {
<div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
<button id="toggle-blocklist-button" class="publish-card-button">Add/Remove blockedUsers</button>
<button id="toggle-blocklist-button" class="publish-card-button">Show/Hide blockedUsers</button>
<button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button>
</div>

View File

@ -140,27 +140,17 @@ const loadMinterBoardPage = async () => {
})
document.getElementById("time-range-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(minterCardIdentifierPrefix)
// Only re-load the cards whenever user presses the refresh button.
})
document.getElementById("sort-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(minterCardIdentifierPrefix)
// Only re-load the cards whenever user presses the refresh button.
})
const showExistingCardsCheckbox = document.getElementById('show-existing-checkbox')
if (showExistingCardsCheckbox) {
showExistingCardsCheckbox.addEventListener('change', async (event) => {
await loadCards(minterCardIdentifierPrefix)
})
}
await featureTriggerCheck()
await loadCards(minterCardIdentifierPrefix)
// Only load the cards whenever user presses the refresh button.
}
const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix
if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) {
@ -439,31 +429,15 @@ 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 = []
let alreadyMinterCards = []
cardsContainer.innerHTML = ''
for (const card of finalCards) {
for (const card of sortedFinalCards) {
try {
const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
@ -624,71 +598,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) {
@ -891,236 +800,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}`)
@ -1492,20 +1171,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()
@ -1963,26 +1628,6 @@ const getMinterAvatar = async (minterName) => {
}
}
const getNewestCommentTimestamp = async (cardIdentifier) => {
try {
// fetchCommentsForCard returns resources each with at least 'created' or 'updated'
const comments = await fetchCommentsForCard(cardIdentifier)
if (!comments || comments.length === 0) {
// No comments => fallback to 0 (or card's own date, if you like)
return 0
}
// The newest can be determined by comparing 'updated' or 'created'
const newestTimestamp = comments.reduce((acc, c) => {
const cTime = c.updated || c.created || 0
return (cTime > acc) ? cTime : acc
}, 0)
return newestTimestamp
} catch (err) {
console.error('Failed to get newest comment timestamp:', err)
return 0
}
}
const deleteCard = async (cardIdentifier) => {
try {
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")

View File

@ -78,28 +78,28 @@ document.addEventListener("DOMContentLoaded", async () => {
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault()
if (!userState.isLoggedIn) {
await login()
}
await loadForumPage();
await loadForumPage()
loadRoomContent("general")
startPollingForNewMessages()
createScrollToTopButton()
if (!userState.isLoggedIn) {
await login()
}
})
})
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]')
minterBoardLinks.forEach(link => {
link.addEventListener("click", async (event) => {
event.preventDefault();
if (!userState.isLoggedIn) {
await login()
}
event.preventDefault()
if (typeof loadMinterBoardPage === "undefined") {
console.log("loadMinterBoardPage not found, loading script dynamically...")
await loadScript("./assets/js/MinterBoard.js")
}
await loadMinterBoardPage()
if (!userState.isLoggedIn) {
await login()
}
})
})
@ -107,15 +107,14 @@ document.addEventListener("DOMContentLoaded", async () => {
addRemoveAdminLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault()
// Possibly require user to login if not logged
if (!userState.isLoggedIn) {
await login()
}
if (typeof loadMinterBoardPage === "undefined") {
console.log("loadMinterBoardPage not found, loading script dynamically...")
await loadScript("./assets/js/MinterBoard.js")
}
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-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;">
<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>
</div>
</div>
@ -468,6 +470,7 @@ let selectedImages = []
let selectedFiles = []
let multiResource = []
let attachmentIdentifiers = []
let editMessageIdentifier = null
// Set up file input handling
const setupFileInputs = (room) => {
@ -558,9 +561,14 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
// Handle send message
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
const messageIdentifier = room === "admins"
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
: `${messageIdentifierPrefix}-${room}-${randomID()}`
let messageIdentifier
if (editMessageIdentifier) {
messageIdentifier = editMessageIdentifier
} else {
messageIdentifier = room === "admins"
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
: `${messageIdentifierPrefix}-${room}-${randomID()}`
}
try {
// Process selected images
@ -672,6 +680,34 @@ const handleDeleteMessage = async (room, existingMessageIdentifier) => {
}
}
const handleEditMessage = async (room, existingMessageIdentifier) => {
try {
const editedMessageObject = {
messageHtml: "", // TODO: Add Quill editor content here
hasAttachment: false,
attachments: [],
replyTo: null
}
const base64Message = btoa(JSON.stringify(editedMessageObject))
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("Editing forum message...")
await qortalRequest(request)
} catch (err) {
console.error("Error editing message:", err)
}
}
function clearInputs() {
// Clear the file input elements and preview container
document.getElementById('file-input').value = ''
@ -689,6 +725,7 @@ function clearInputs() {
attachmentIdentifiers = []
selectedImages = []
selectedFiles = []
editMessageIdentifier = null
// Remove the reply container
const replyContainer = document.querySelector('.reply-container')
@ -788,6 +825,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
handleReplyLogic(fetchMessages)
handleDeleteLogic(fetchMessages, room)
handleEditLogic(fetchMessages, room)
await updatePaginationControls(room, limit)
} catch (error) {
@ -1008,6 +1046,7 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const attachmentHtml = await buildAttachmentHtml(message, room)
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
let deleteButtonHtml = ''
let editButtonHtml = ''
if (message.name === userState.accountName) {
deleteButtonHtml = `
<button class="delete-button"
@ -1016,6 +1055,13 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
Delete
</button>
`
editButtonHtml = `
<button class="edit-button"
data-message-identifier="${message.identifier}"
data-room="${room}">
Edit
</button>
`
}
return `
@ -1034,7 +1080,7 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
${attachmentHtml}
</div>
<div class="message-actions">
${deleteButtonHtml}
${deleteButtonHtml}${editButtonHtml}
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
</div>
@ -1206,6 +1252,23 @@ const handleDeleteLogic = (fetchMessages, room) => {
})
}
const handleEditLogic = (fetchMessages, room) => {
// Only select buttons that do NOT already have a listener
const editButtons = document.querySelectorAll('.edit-button:not(.bound-edit)')
editButtons.forEach(button => {
button.classList.add('bound-edit')
button.addEventListener('click', async () => {
const existingMessageIdentifier = button.dataset.messageIdentifier
const originalMessage = messagesById[existingMessageIdentifier]
if (!originalMessage) return
editMessageIdentifier = existingMessageIdentifier
const quill = new Quill('#editor')
quill.clipboard.dangerouslyPasteHTML(originalMessage.content)
// Optionally show a small notice: "Editing message..."
})
})
}
const showReplyPreview = (repliedMessage) => {
replyToMessageIdentifier = repliedMessage.identifier

View File

@ -205,4 +205,417 @@ const fetchAllInviteTransactions = async () => {
}
}
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._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
continue
}
// Fetch poll results
const pollResults = await fetchPollResults(pollName)
if (!pollResults) {
card._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
continue
}
// Process them
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
card.decryptedCardData.creator,
card.card.identifier
)
card._adminYes = adminYes
card._adminNo = adminNo
card._minterYes = minterYes
card._minterNo = minterNo
} else {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll);
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
card._adminYes = adminYes
card._adminNo = adminNo
card._minterYes = minterYes
card._minterNo = minterNo
}
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
card._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
}
}
if (ascending) {
// least votes first
cards.sort((a, b) => {
const diffAdminYes = a._adminYes - b._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffAdminNo = b._adminNo - a._adminNo
if (diffAdminNo !== 0) return diffAdminNo
const diffMinterYes = a._minterYes - b._minterYes
if (diffMinterYes !== 0) return diffMinterYes
return b._minterNo - a._minterNo
})
} else {
// most votes first
cards.sort((a, b) => {
const diffAdminYes = b._adminYes - a._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffAdminNo = a._adminNo - b._adminNo
if (diffAdminNo !== 0) return diffAdminNo
const diffMinterYes = b._minterYes - a._minterYes
if (diffMinterYes !== 0) return diffMinterYes
return a._minterNo - b._minterNo
})
}
}
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
}
}