Compare commits

...

15 Commits

Author SHA1 Message Date
731c53b5d4 removed info 2025-04-05 16:38:50 -07:00
20f9845610 bump version 1.22 2025-04-05 16:38:01 -07:00
5b49f0d4fc Added additional checks for expired transactions, and changed display on the MAM board, also added check for promotionCard so that previous promotions are kept separate. 2025-04-05 16:37:15 -07:00
b2dde1ea56 Resolved card loading issue on MAM Board. 2025-03-12 18:09:08 -07:00
5630f80a54 Version 1.2 includes a dramatic re-write to the card loading on the Minter and MAM Boards, cards now load at least 25x faster. This should be a significant improvement for all. Hence the significant version increase. 2025-02-27 17:56:20 -08:00
59bd5cc760 Bump to v1.06.6b 2025-02-27 15:46:36 -08:00
6f459d7e0a Fixed issue with display of pending invites on AdminTools page. 2025-02-27 15:45:55 -08:00
fe230a91d3 added initial README 2025-02-27 12:41:44 -08:00
5443d159b0 removed tracked files from repo 2025-02-27 12:25:27 -08:00
e0c5a09378 change to version 1.06.4b, modified MinterBoard Card Filtering and header options, header layout, and header itself. Will modify other boards in the future. Default for card display is now 45 days, and it has been made more clear in regard to what the selected options do. Color changes on buttons as well. 2025-02-22 18:05:35 -08:00
509e3bf357 Modified default to 'Published Within Last 45 Days', and changed wording on selector as such. 2025-02-22 14:36:57 -08:00
07f4fa3e6e Added checks for poll upon duplicate publish, allowing a new poll to be published if the new publish doesn't contain one. 2025-02-22 14:27:10 -08:00
7afa06623f version 1.06.3b - resolved issue wherein the 'last x days' display of cards on the various boards was not working properly. This was due to the 'searchSimple' function not passing the correct parameters to the ultimate API call. 2025-02-08 12:26:52 -08:00
51921992e2 Fix for kick/ban transactions on AdminBoard, and fix for tx creation showing up improperly. 2025-02-05 20:01:01 -08:00
f5ce634ff5 Forgot to publish this update to git 2025-02-03 09:34:23 -08:00
13 changed files with 959 additions and 313 deletions

2
.gitignore vendored
View File

@ -1,2 +0,0 @@
/.vscode
/.sync*

Binary file not shown.

Binary file not shown.

49
README.md Normal file
View File

@ -0,0 +1,49 @@
### Q-Mintership-Alpha
Q-Mintership-Alpha is the currently utilized version of the Q-Mintership app published on qortal://APP/Q-Mintership.
As of Feb 27 2025 Q-Mintership-Alpha is still the published and utilized version of the app.
#### Q-Mintership's 'MinterBoard'
The MinterBoard of Q-Mintership, is the primary location for users to publish 'cards' with information about themselves, links to things they have published on QDN, etc... and obtain minting rights from the Minter Admins.
- Cards are created by any non-minter (either previous minter no longer in the group, or new accounts without minting rights).
- Existing community members, existing minters, and Minter Admins, can vote/comment on the cards.
- Once a card has obtained the minimum required number of admin votes (40% of the Minter Admin count), the card will then display additional features to those that have the rights to see them. (Minter Admins, and 'Forum Admins', however 'Forum Admins' cannot actually make use of the functionality, they are only able to view it for development purposes.)
- The Minter Admins then initiate a PENDING GROUP_INVITE transaction.
- The Minter Admins are then able to issue GROUP_APPROVAL transactions to approve the invite.
- Once the required number of GROUP_APPROVAL transactions have been created, the GROUP_INVITE is no longer pending, and is active.
- The would-be minter that published a card, can then see a new 'JOIN_GROUP' button on their card upon returning to the MinterBoard.
- The user will then JOIN_GROUP to the MINTER group, ID 694. Thus allowing the ability to mint.
- Upon joining the MINTER group, the user will then have the ability to create a MINTING KEY (the same way as it was created prior to the Mintership concept, however now it no longer requires users to be level 1, only requirement now is to be part of the MINTER group)
- User then assigns their key to their node, and starts minting.
#### Q-Mintership 'AdminBoard'
- The AdminBoard is a separate board, encrypted to admins only, meant to be utilized for private decision-making between the admins.
- The AdminBoard was also adapted to allow REMOVAL of MINTER group members, via GROUP_APPROVAL from the Minter Admins.
- The REMOVAL functionality, at the moment, is private. Meaning only the admins that have access to the AdminBoard, can see the data. This will be changed in the future, and a new location where the data will be able to be seen publicly, will be created.
#### Q-Mintership Forum
- The Forum portion of Q-Mintership is a public (and private) forum, allowing communications to take place in the fashion of long-term forum messages, replies, etc.
- Publishing of images with previews, and various 'attachments' with data is also possible on the forum.
- The forum has two public rooms by default, and one private room. General and Minter rooms are public, and Admin room is private.
- The forum will be getting extensive updates in the future, and the Minter room will be made a publicly VIEWABLE room, but only able to be published to by MINTERS.
#### Q-Mintership MAM Board
- The MAM Board (or ARBoard in the code) is built to allow the adding and removal of Minter Admins from the MINTER group. Proposals for additions or removals of certain accounts from and to the Minter takes place here.
- This board also displays a list of the current Minter Admins, and has the ability to propose a removal of that user with a propose removal button.
#### Additional
Many additional features and functions are planned for Q-Mintership, and increased performance and more will be added as time goes on.
Longer-term the plan is to re-write the app into React+TypeScript, which will make it MUCH faster and able to accommodate much more, with a component-based development style similar to that of the other React-based applications on Qortal (Q-Tube, Q-Blog, Q-Mail, etc.)
A fully featured data viewer and explorer function will be built into Q-Mintership in the future, along with a comprehensive notification system, and more.

View File

@ -584,7 +584,7 @@ body {
border-radius: 5px; border-radius: 5px;
padding: 20px; padding: 20px;
margin: 20px auto; /* center horizontally */ margin: 20px auto; /* center horizontally */
max-width: 600px; /* limit width */ /* max-width: 600px; */
color: #ddd; /* text color */ color: #ddd; /* text color */
text-align: center; text-align: center;
align-items: center; align-items: center;
@ -596,7 +596,7 @@ body {
background-color:#000000; background-color:#000000;
width: 90%; width: 90%;
font-size: 1.8rem; font-size: 1.8rem;
color: #4d0000; color: #fff3f3;
text-align: center; text-align: center;
align-items: center; align-items: center;
/* you could style the list items or bullet if you like */ /* you could style the list items or bullet if you like */
@ -616,7 +616,17 @@ body {
background-color: #14161a; background-color: #14161a;
border: 1px solid #8caeb0; border: 1px solid #8caeb0;
border-radius: 4px; border-radius: 4px;
color: #5c0101; color: #f19c9c;
}
.invite-form input.invite-input {
padding: 1rem;
font-size: 2rem;
line-height: 2;
background-color: #14161a;
border: 1px solid #8caeb0;
border-radius: 4px;
color: #dddddd;
} }
.publish-card-button { .publish-card-button {
@ -707,30 +717,42 @@ body {
background-color: #281e1e; background-color: #281e1e;
} }
.approve-invite-list-button {
/* Responsive design */ background-color: rgba(32, 88, 34, 0.554);
@media (max-width: 768px) { color: #fff;
.publish-card-view { border: none;
width: 90%; border-radius: 1vw;
padding: 2vh; padding: 1vh,2vh;
} cursor: pointer;
font-size: 1.1rem;
.publish-card-button { transition: background-color 0.2s ease;
font-size: 1.8vh;
padding: 1.5vh;
}
.publish-card-form button {
font-size: 1.8vh;
padding: 1.2vh;
}
} }
.refresh-cards-button { .approve-invite-list-button:hover {
border-color: white; background-color: rgba(34, 186, 47, 0.84); /* a darker variant */
border-radius: 1.5vh; }
background-color: black;
color: white; .invite-approvals strong {
display: inline-block;
}
.invite-item {
margin-bottom: 0.5em;
background-color: rgba(31, 31, 31, 0.595);
border: 1px solid #444;
border-radius: 6px;
color: #ccc;
padding: 1rem;
}
/* Top row: use flex for horizontal arrangement */
.invite-top-row {
display: flex;
background-color:#173c52ae;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: center;
} }
/* Responsive Design */ /* Responsive Design */
@ -754,8 +776,14 @@ body {
.refresh-cards-button { .refresh-cards-button {
border-color: white; border-color: white;
border-radius: 1.5vh; border-radius: 1.5vh;
background-color: black; background-color: rgba(0, 0, 0, 0.089);
color: white; color: white;
font-size: 0.9rem;
}
.refresh-cards-button:hover {
background-color: rgba(35, 129, 136, 0.137);
color: rgba(90, 201, 221, 0.793);
} }
/* Two cards per row on medium screens */ /* Two cards per row on medium screens */

View File

@ -60,11 +60,13 @@ const loadAddRemoveAdminPage = async () => {
<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="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">All Creation Dates</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 Day</option>
<option value="7">Last 7 days</option> <option value="7">Last 7 Days</option>
<option value="30" selected>Last 30 days</option> <option value="30">...Within 30 Days</option>
<option value="90">Last 90 days</option> <option value="45" selected>Published Within Last 45 Days</option>
<option value="60">...Within 60 Days</option>
<option value="90">...Within 90 Days</option>
</select> </select>
</div> </div>
<div id="cards-container" class="cards-container" style="margin-top: 1rem""> <div id="cards-container" class="cards-container" style="margin-top: 1rem"">
@ -95,6 +97,7 @@ const loadAddRemoveAdminPage = async () => {
document.getElementById("refresh-cards-button").addEventListener("click", async () => { document.getElementById("refresh-cards-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Refreshing cards...</p>" cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
await initializeCachedGroups()
await loadCards(addRemoveIdentifierPrefix) await loadCards(addRemoveIdentifierPrefix)
}) })
@ -117,6 +120,13 @@ const loadAddRemoveAdminPage = async () => {
linksContainer.appendChild(newLinkInput) 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) => { document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault() event.preventDefault()
await publishARCard(addRemoveIdentifierPrefix) await publishARCard(addRemoveIdentifierPrefix)
@ -159,51 +169,63 @@ const fetchAllARTxData = async () => {
txGroupId: 694, txGroupId: 694,
}) })
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs) const { finalAddTxs, pendingAddTxs, expiredAddTxs } = partitionAddTransactions(allAddTxs)
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs) const { finalRemTxs, pendingRemTxs, expiredRemTxs } = partitionRemoveTransactions(allRemTxs)
// We are going to keep all transactions in order to filter more accurately for display purposes. // We are going to keep all transactions in order to filter more accurately for display purposes.
console.log('Final addAdminTxs:', finalAddTxs); console.log('Final addAdminTxs:', finalAddTxs)
console.log('Pending addAdminTxs:', pendingAddTxs); console.log('Pending addAdminTxs:', pendingAddTxs)
console.log('Final remAdminTxs:', finalRemTxs); console.log('expired addAdminTxs', expiredAddTxs)
console.log('Pending remAdminTxs:', pendingRemTxs); console.log('Final remAdminTxs:', finalRemTxs)
console.log('Pending remAdminTxs:', pendingRemTxs)
console.log('expired remAdminTxs', expiredRemTxs)
return { return {
finalAddTxs, finalAddTxs,
pendingAddTxs, pendingAddTxs,
expiredAddTxs,
finalRemTxs, finalRemTxs,
pendingRemTxs, pendingRemTxs,
expiredRemTxs
} }
} }
const partitionAddTransactions = (rawTransactions) => { const partitionAddTransactions = (rawTransactions) => {
const finalAddTxs = [] const finalAddTxs = []
const pendingAddTxs = [] const pendingAddTxs = []
const expiredAddTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') { for (const tx of rawTransactions) {
pendingAddTxs.push(tx) if (tx.approvalStatus === 'PENDING') {
} else { pendingAddTxs.push(tx)
finalAddTxs.push(tx)
}
} }
else if (tx.approvalStatus === 'EXPIRED'){
return { finalAddTxs, pendingAddTxs }; expiredAddTxs.push(tx)
} else {
finalAddTxs.push(tx)
}
}
return { finalAddTxs, pendingAddTxs, expiredAddTxs };
} }
const partitionRemoveTransactions = (rawTransactions) => { const partitionRemoveTransactions = (rawTransactions) => {
const finalRemTxs = [] const finalRemTxs = []
const pendingRemTxs = [] const pendingRemTxs = []
const expiredRemTxs = []
for (const tx of rawTransactions) { for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') { if (tx.approvalStatus === 'PENDING') {
pendingRemTxs.push(tx) pendingRemTxs.push(tx)
} else { }
else if (tx.approvalStatus === 'EXPIRED'){
expiredRemTxs.push(tx)
} else {
finalRemTxs.push(tx) finalRemTxs.push(tx)
} }
} }
return { finalRemTxs, pendingRemTxs } return { finalRemTxs, pendingRemTxs, expiredRemTxs }
} }
@ -820,9 +842,9 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier) const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
actionsHtml = actionsHtmlCheck actionsHtml = actionsHtmlCheck
const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData() const { finalAddTxs, pendingAddTxs, expiredAddTxs, finalRemTxs, pendingRemTxs, expiredRemTxs } = await fetchAllARTxData()
const confirmedAdd = finalAddTxs.some( const userConfirmedAdd = finalAddTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress (tx) => tx.groupId === 694 && tx.member === accountAddress
) )
const userPendingAdd = pendingAddTxs.some( const userPendingAdd = pendingAddTxs.some(
@ -834,31 +856,88 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
const userPendingRemove = pendingRemTxs.some( const userPendingRemove = pendingRemTxs.some(
(tx) => tx.groupId === 694 && tx.admin === accountAddress (tx) => tx.groupId === 694 && tx.admin === accountAddress
) )
const userExpiredAdd = expiredAddTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const userExpiredRem = expiredRemTxs.some(
(tx) => tx.groupId === 694 && tx.admin === accountAddress
)
const noExpired = (!userExpiredAdd && !userExpiredRem)
// If user is definitely admin (finalAdd) and not pending removal // If user is definitely admin (finalAdd) and not pending removal
if (confirmedAdd && !userPendingRemove && existingAdmin) { if (userConfirmedAdd && !userPendingRemove && !userPendingAdd && noExpired && existingAdmin && promotionCard) {
console.warn(`account was already admin, final. no add/remove pending.`); console.warn(`account was already admin, final. no add/remove pending.`);
cardColorCode = 'rgb(3, 11, 24)' cardColorCode = 'rgb(3, 11, 24)'
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`; altText = `<h4 style="color:rgb(89, 191, 204); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
actionsHtml = '' actionsHtml = ''
} }
if (confirmedAdd && userPendingRemove && existingAdmin) { if (userConfirmedAdd && !userPendingRemove && userExpiredRem && existingAdmin && promotionCard) {
console.warn(`Account has previously had a removal attempt expire`);
cardColorCode = 'rgb(33, 40, 11)'
altText = `<h4 style="color:rgb(136, 114, 146); margin-bottom: 0.5em;">PROMOTED, (+Previous Failed Demotion).</h4>`;
actionsHtml = ''
}
if (userConfirmedAdd && !userPendingRemove && userExpiredAdd && existingAdmin && promotionCard) {
console.warn(`Account has previously had a removal attempt expire`);
cardColorCode = 'rgb(14, 3, 24)'
altText = `<h4 style="color:rgb(114, 117, 146); margin-bottom: 0.5em;">PROMOTED, (+Previous Failed Promotion).</h4>`;
actionsHtml = ''
}
if (userConfirmedAdd && userPendingRemove && existingAdmin && noExpired && !promotionCard) {
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`) console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
altText = `<h4 style="color:rgb(85, 34, 34); margin-bottom: 0.5em;">Pending REMOVAL in progress...</h4>`;
}
if (userConfirmedAdd && userPendingRemove && existingAdmin && userExpiredAdd && !promotionCard) {
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
altText = `<h4 style="color:rgb(85, 74, 34); margin-bottom: 0.5em;">Pending REMOVAL in progress... (+Previous Failed Promotion)</h4>`;
}
if (userConfirmedAdd && userPendingRemove && existingAdmin && userExpiredRem && !promotionCard) {
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
altText = `<h4 style="color:rgb(198, 26, 13); margin-bottom: 0.5em;">Pending REMOVAL in progress... (+Previous Failed Demotion)</h4>`;
} }
// If user has a final "remove" and no pending additions or removals // If user has a final "remove" and no pending additions or removals and no expired transactions
if (confirmedRemove && !userPendingAdd && existingMinter) { if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && noExpired && !promotionCard) {
console.warn(`account was demoted, final. no add pending, existingMinter.`); console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
cardColorCode = 'rgb(29, 4, 6)' cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>` altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
actionsHtml = '' actionsHtml = ''
} }
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && userExpiredRem && !promotionCard) {
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(170, 32, 48); margin-bottom: 0.5em;">DEMOTED (+Previous Failed Demotion)</h4>`
actionsHtml = ''
}
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && userExpiredAdd && !promotionCard) {
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(119, 170, 32); margin-bottom: 0.5em;">DEMOTED (+Previous Failed Promotion)</h4>`
actionsHtml = ''
}
// If user has both final remove and pending add, do something else // If user has both final remove and pending add, do something else
if (confirmedRemove && userPendingAdd && existingMinter) { if (confirmedRemove && userPendingAdd && existingMinter && noExpired && promotionCard) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`) console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
// Possibly show "DEMOTED but re-add in progress" or something altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...</h4>`
}
if (confirmedRemove && userPendingAdd && existingMinter && userExpiredAdd && promotionCard) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...(+Previous Failed Promotion)</h4>`
}
if (confirmedRemove && userPendingAdd && existingMinter && userExpiredRem && promotionCard) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...(+Previous Failed Demotion)</h4>`
} }
} else if ( verifiedName && illegalDuplicate) { } else if ( verifiedName && illegalDuplicate) {

View File

@ -72,8 +72,7 @@ const loadAdminBoardPage = async () => {
mainContent.innerHTML = ` mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;"> <div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">AdminBoard</h1> <h1 style="color: lightblue;">AdminBoard</h1>
<p style="font-size: 1.25em;"> The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins. Any Admin may publish a card, and related data, make comments on existing cards, and vote on existing card data in support or not of the name on the card. It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding/removing from the minter group. </p> <p style="font-size: 0.95rem; color:rgba(255, 255, 255, 0.53)"> The Admin Board was meant to be utilized for DECISIONS regarding Minters or would-be Minters, and is encrypted to the Admins so that the data for the DECISIONS remains private. However, it later became the location to REMOVE minters as well. This, not being the original intended purpose has become problematic, as the removal data SHOULD be public. In the future, this data WILL be made public. The Admin Board will continue to be utilized for decision-making, but will NOT be a place for hidden removal data only. </p>
<p> More functionality will be added over time. One of the first features will be the ability to output the existing card data 'decisions', to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed, and the MINTER group is transferred to 'null'.</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button> <button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button> <button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(70, 106, 105); background-color: black;"> <select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(70, 106, 105); background-color: black;">
@ -84,12 +83,14 @@ const loadAdminBoardPage = async () => {
<option value="most-votes">Most Votes</option> <option value="most-votes">Most Votes</option>
</select> </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">All Creation Dates</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 Day</option>
<option value="7">Last 7 days</option> <option value="7">Last 7 Days</option>
<option value="30" selected>Last 30 days</option> <option value="30">...Within 30 Days</option>
<option value="90">Last 90 days</option> <option value="45" selected>Published Within Last 45 Days</option>
</select> <option value="60">...Within 60 Days</option>
<option value="90">...Within 90 Days</option>
</select>
<div class="show-card-checkbox" style="margin-top: 1em;"> <div class="show-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" /> <input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label> <label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
@ -1078,7 +1079,7 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
const handleKickMinter = async (minterName) => { const handleKickMinter = async (minterName) => {
try { try {
isAddress = await getAddressInfo(minterName) let isAddress = await getAddressInfo(minterName)
// Optional block check // Optional block check
let txGroupId = 0 let txGroupId = 0
@ -1091,7 +1092,7 @@ const handleKickMinter = async (minterName) => {
// Get the minter address from name info // Get the minter address from name info
let minterAddress let minterAddress
if (!isAddress){ if (!isAddress.address || !isAddress.address != minterName){
const minterNameInfo = await getNameInfo(minterName) const minterNameInfo = await getNameInfo(minterName)
minterAddress = minterNameInfo?.owner minterAddress = minterNameInfo?.owner
} else { } else {
@ -1107,7 +1108,7 @@ const handleKickMinter = async (minterName) => {
const reason = 'Kicked by Minter Admins' const reason = 'Kicked by Minter Admins'
const fee = 0.01 const fee = 0.01
const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee) const rawKickTransaction = await createGroupKickTransaction(adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
const signedKickTransaction = await qortalRequest({ const signedKickTransaction = await qortalRequest({
action: "SIGN_TRANSACTION", action: "SIGN_TRANSACTION",
@ -1138,7 +1139,7 @@ const handleKickMinter = async (minterName) => {
} }
const handleBanMinter = async (minterName) => { const handleBanMinter = async (minterName) => {
isAddress = await getAddressInfo(minterName) let isAddress = await getAddressInfo(minterName)
try { try {
let txGroupId = 0 let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo() // const { height: currentHeight } = await getLatestBlockInfo()
@ -1151,9 +1152,9 @@ const handleBanMinter = async (minterName) => {
txGroupId = 694 txGroupId = 694
} }
let minterAddress let minterAddress
if (!isAddress) { if (!isAddress.address || !isAddress.address != minterName){
const minterNameInfo = await getNameInfo(minterName) const minterNameInfo = await getNameInfo(minterName)
const minterAddress = minterNameInfo?.owner minterAddress = minterNameInfo?.owner
} else { } else {
minterAddress = minterName minterAddress = minterName
} }

View File

@ -18,22 +18,23 @@ const loadMinterAdminToolsPage = async () => {
mainContent.innerHTML = ` mainContent.innerHTML = `
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R"> <div class="tools-main mbr-parallax-background cid-ttRnlSkg2R">
<div class="tools-header" style="color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 10px;"> <div class="tools-header" style="color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 10px;">
<div><h1 style="font-size: 50px; margin: 0;">Admin Tools</h1></div>
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;"> <div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; 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;"> <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'}'s Admin Tools</span>
</div> </div>
<div><h2>Welcome to Admin Tools</h2></div>
<div> <div>
<p>On this page you will find admin functionality for the Q-Mintership App. Including the 'blockList' for blocking comments from certain names, and manual creation of invite transactions.</p> <p style="color:rgba(80, 9, 9, 0.63)"></p>
<p>More features will be added as time goes on. This is the start of the functionality here.</p> <p style="color:rgb(82, 114, 145)"> The approve feature allows invite by name, and shows ALL existing approvals ongoing, whether initiated on this page manually or not. Allowing for easy displaying and approving without loading the MinterBoard and scrolling through cards. </p>
<p style="font-size: 0.85rem"> This is NOT a substitute for the AdminBoard, as obviously no data regarding the account is published here. However, if you have already read the data there, and wish to see it easily in one place, here, that is fine. It can also obviously be utilized to manually invite users that require such actions to be taken, however, this action as well should be extremely limited in usage, and not leveraged without extensive provided rationale.</p>
</div> </div>
</div> </div>
<div id="tools-submenu" class="tools-submenu"> <div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;"> <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" style="background-color:rgba(80, 9, 9, 0.63)" class="publish-card-button">Add/Remove blockedUsers</button>
<button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button> <button id="create-group-invite" class="publish-card-button" style="background-color:rgb(82, 114, 145)">Create and Display Pending Group Invites</button>
</div> </div>
<div id="tools-window" class="tools-window" style="margin-top: 2em;"> <div id="tools-window" class="tools-window" style="margin-top: 2em;">
@ -55,6 +56,30 @@ const loadMinterAdminToolsPage = async () => {
<button id="blocklist-remove-button" class="publish-card-button">Remove</button> <button id="blocklist-remove-button" class="publish-card-button">Remove</button>
</div> </div>
</div> </div>
<div id="invite-container" class="invite-form" style="display: none; flex-direction: column; padding: 0.75em; align-items: center; justify-content: center;">
<!-- Existing pending invites display -->
<div id="pending-invites-display" class="pending-invites-display" style="margin-bottom: 1em;">
<!-- We will fill this dynamically with a list/table of pending invites -->
</div>
<!-- Input for name/address -->
<h3 style="margin-top: 0;">Manual Group Invite</h3>
<input
type="text"
id="invite-input"
class="invite-input"
placeholder="Enter name or address to invite"
style="margin-bottom: 1em;"
/>
<!-- Button to create the invite transaction -->
<div class="invite-button-container publish-card-form">
<button id="invite-user-button" class="publish-card-button">Invite User</button>
</div>
</div>
</div> </div>
</div> </div>
@ -63,10 +88,10 @@ const loadMinterAdminToolsPage = async () => {
document.body.appendChild(mainContent) document.body.appendChild(mainContent)
addToolsPageEventListeners() await addToolsPageEventListeners()
} }
function addToolsPageEventListeners() { const addToolsPageEventListeners= async () => {
document.getElementById("toggle-blocklist-button").addEventListener("click", async () => { document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
const container = document.getElementById("blocklist-container") const container = document.getElementById("blocklist-container")
// toggle show/hide // toggle show/hide
@ -116,6 +141,32 @@ function addToolsPageEventListeners() {
alert(`"${nameToRemove}" removed from the block list (if it was present).`) alert(`"${nameToRemove}" removed from the block list (if it was present).`)
}) })
document.getElementById("invite-user-button").addEventListener("click", async () => {
const inviteInput = document.getElementById("invite-input")
const nameOrAddress = inviteInput.value.trim()
if (!nameOrAddress) return
try {
// We'll call some function handleManualInvite(nameOrAddress)
await handleManualInvite(nameOrAddress)
inviteInput.value = ""
} catch (err) {
console.error("Error inviting user:", err)
alert("Failed to invite user.")
}
})
document.getElementById("create-group-invite").addEventListener("click", async () => {
const inviteContainer = document.getElementById("invite-container")
// Toggle display
inviteContainer.style.display = (inviteContainer.style.display === "none" ? "flex" : "none")
// If showing, load the pending invites
if (inviteContainer.style.display === "flex") {
const pendingInvites = await fetchPendingInvites()
await displayPendingInviteDetails(pendingInvites)
}
})
} }
const displayBlockList = (blockedNames) => { const displayBlockList = (blockedNames) => {
@ -131,4 +182,139 @@ const displayBlockList = (blockedNames) => {
` `
} }
const fetchPendingInvites = async () => {
try {
const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions()
return pendingInviteTxs
} catch (err) {
console.error("Error fetching pending invites:", err)
return []
}
}
const handleManualInvite = async (nameOrAddress) => {
const addressInfo = await getAddressInfo(nameOrAddress)
let address = addressInfo.address
if (addressInfo && address) {
console.log(`address is ${address}`)
} else {
// it might be a Qortal name => getNameInfo
const nameData = await getNameInfo(nameOrAddress)
if (!nameData || !nameData.owner) {
throw new Error(`Cannot find valid address for ${nameOrAddress}`)
}
address = nameData.owner
}
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const timeToLive = 864000 // e.g. 10 days in seconds
const fee = 0.01
let txGroupId = 694
// build the raw invite transaction
const rawInviteTransaction = await createGroupInviteTransaction(
address,
adminPublicKey,
694,
address,
timeToLive,
txGroupId,
fee
)
// sign
const signedTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: rawInviteTransaction
})
if (!signedTransaction) {
throw new Error("SIGN_TRANSACTION returned null. Possibly user canceled or an older UI?")
}
// process
const processResponse = await processTransaction(signedTransaction)
if (!processResponse) {
throw new Error("Failed to process transaction. Possibly canceled or error from Qortal Core.")
}
alert(`Invite transaction submitted for ${nameOrAddress}. Wait for confirmation.`)
}
const displayPendingInviteDetails = async (pendingInvites) => {
const invitesContainer = document.getElementById('pending-invites-display')
if (!pendingInvites || pendingInvites.length === 0) {
invitesContainer.innerHTML = "<p>No pending invites found.</p>"
return
}
let html = `<h4>Current Pending Invites:</h4><div class="pending-invites-list">`
for (const inviteTx of pendingInvites) {
const inviteeAddress = inviteTx.invitee
const dateStr = new Date(inviteTx.timestamp).toLocaleString()
let inviteeName = ""
const txSig = inviteTx.signature
const creatorName = await getNameFromAddress(inviteTx.creatorAddress)
if (!creatorName) {
creatorName = inviteTx.creatorAddress
}
try {
// fetch the name from address, if it fails we keep it blank or fallback to the address
inviteeName = await getNameFromAddress(inviteeAddress)
if (!inviteeName || inviteeName === inviteeAddress) {
inviteeName = inviteeAddress // fallback
}
} catch (err) {
inviteeName = inviteeAddress // fallback if getName fails
}
const approvalSearchResults = await searchTransactions({
txTypes: ['GROUP_APPROVAL'],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: false,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0
})
const approvals = approvalSearchResults.filter(
(approvalTx) => approvalTx.pendingSignature === txSig
)
const { tableHtml, approvalCount = approvals.length } = await buildApprovalTableHtml(approvals, getNameFromAddress)
const finalTable = approvals.length > 0 ? tableHtml : "<p>No Approvals Found</p>"
html += `
<div class="invite-item">
<div class="invite-top-row">
<span><strong>Invite Tx</strong>:<p style="color:lightblue"> ${inviteTx.signature.slice(0, 8)}...</p></span>
<span> <strong>Invitee</strong>:<p style="color:lightblue"> ${inviteeName}</p></span>
<span> <strong>Date</strong>:<p style="color:lightblue"> ${dateStr}</p></span>
<span> <strong>CreatorName</strong>:<p style="color:lightblue"> ${creatorName}</p></span>
<span> <strong>Total Approvals</strong>:<p style="color:lightblue"> ${approvalCount}</p></span>
</div>
<!-- Next line for approvals -->
<div class="invite-approvals">
<strong>Existing Approvals:</strong>
${finalTable}
</div>
<button
class="approve-invite-list-button"
onclick="handleGroupApproval('${inviteTx.signature}')"
>
Approve Invite
</button>
</div>
`
}
html += "</div>"
invitesContainer.innerHTML = html
}

View File

@ -9,6 +9,8 @@ const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 2012800 //TODO update this to corr
let featureTriggerPassed = false let featureTriggerPassed = false
let isApproved = false let isApproved = false
let cachedMinterAdmins
let cachedMinterGroup
const loadMinterBoardPage = async () => { const loadMinterBoardPage = async () => {
// Clear existing content on the page // Clear existing content on the page
@ -25,42 +27,99 @@ const loadMinterBoardPage = async () => {
const publishButtonColor = '#527c9d' const publishButtonColor = '#527c9d'
const minterBoardNameColor = '#527c9d' const minterBoardNameColor = '#527c9d'
mainContent.innerHTML = ` mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;"> <div class="minter-board-main" style="padding: 0.5vh; text-align: center;">
<h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
<p style="font-size: 1.25em;"> Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!</p> <!-- Board Title + Intro -->
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px; background-color: ${publishButtonColor}">Publish Minter Card</button> <h1 style="color: #527c9d;">The Minter Board</h1>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button> <p style="font-size: 1.2em; color:rgb(85, 119, 101)">
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(38, 106, 106); background-color: black;"> The Minter Board is where Minting Rights are Delegated.
<option value="newest" selected>Sort by Date</option> </p>
<option value="name">Sort by Name</option> <p style="font-size: 1.1em; color:rgb(85, 119, 119)">
<option value="recent-comments">Newest Comments</option> To obtain minting rights, click 'PUBLISH CARD' and create your card. A subsequent vote will approve/deny your card.
<option value="least-votes">Least Votes</option> </p>
<option value="most-votes">Most Votes</option> <p>
</select> After your card has received the necessary invite, return to the card and click the Join Group button to join the MINTER group.
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;"> (A Detailed how-to guide will be coming soon.)
<option value="0">Show All</option> </p>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option> <div class="card-display-options">
<option value="30" selected>Last 30 days</option> <!-- Centered heading -->
<option value="90">Last 90 days</option> <h4 class="options-heading"style="color: #527c9d;">CARD DISPLAY OPTIONS</h4>
</select>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div> <!-- A flex container for all the controls (sort, time range, checkbox) -->
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <div class="options-row">
<form id="publish-card-form" class="publish-card-form"> <!-- Sort by -->
<h3>Create or Update Your Card</h3> <label for="sort-select" class="options-label">Sort By:</label>
<label for="card-header">Header:</label> <select id="sort-select" class="options-select">
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required> <option value="newest" selected>Date</option>
<label for="card-content">Content:</label> <option value="name">Name</option>
<textarea id="card-content" placeholder="Enter detailed information about why you would like to be a minter... the more the better, and links to things you have published on QDN will help a lot! Give the Minter Admins things to make decisions by!" required></textarea> <option value="recent-comments">Newest Comments</option>
<label for="card-links">Links (qortal://...):</label> <option value="least-votes">Least Votes</option>
<div id="links-container"> <option value="most-votes">Most Votes</option>
<input type="text" class="card-link" placeholder="Enter QDN link"> </select>
<!-- Time range -->
<label for="time-range-select" class="options-label">Show Cards:</label>
<select id="time-range-select" class="options-select">
<option value="0">Show ALL Cards Published</option>
<option value="1">...Within Last 1 Day</option>
<option value="7">...Within Last 7 Days</option>
<option value="30">...Within 30 Days</option>
<option value="45" selected>Published Within Last 45 Days</option>
<option value="60">...Within 60 Days</option>
<option value="90">...Within 90 Days</option>
</select>
<!-- Show existing checkbox -->
<label class="options-check">
<input type="checkbox" id="show-existing-checkbox" />
Show Existing Minter Cards (History)
</label>
</div>
</div>
<!-- Card counter heading centered, with actual counter below if desired -->
<div style="margin-bottom: 1em;">
<div style="text-align: center; margin-top: 0.5em;">
<span id="board-card-counter" style="font-size: 1rem; color:rgb(153, 203, 204); padding: 0.5em;">
<!-- e.g. "5 cards found" -->
</span>
</div> </div>
<button type="button" id="add-link-button">Add Another Link</button> </div>
<button type="submit" id="submit-publish-button">Publish Card</button>
<button type="button" id="cancel-publish-button">Cancel</button> <!-- Row for Publish / Refresh actions -->
</form> <div class="card-actions" style="margin-bottom: 1em;">
</div> <button id="publish-card-button" class="publish-card-button">
PUBLISH CARD
</button>
<button id="refresh-cards-button" class="refresh-cards-button"
style="padding: 1vh;">
REFRESH CARDS
</button>
</div>
<!-- Container for displayed cards -->
<div id="cards-container" class="cards-container" style="margin-top: 2vh;"></div>
<!-- Hidden Publish Card Form -->
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 2vh;">
<form id="publish-card-form" class="publish-card-form">
<h3>Create or Update Your Card</h3>
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information about why you would like to be a minter... the more the better..." required>
</textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" id="submit-publish-button">Publish Card</button>
<button type="button" id="cancel-publish-button">Cancel</button>
</form>
</div>
</div> </div>
` `
document.body.appendChild(mainContent) document.body.appendChild(mainContent)
@ -107,11 +166,18 @@ const loadMinterBoardPage = async () => {
}) })
document.getElementById("refresh-cards-button").addEventListener("click", async () => { document.getElementById("refresh-cards-button").addEventListener("click", async () => {
// Update the caches to include any new changes (e.g. new minters)
await initializeCachedGroups()
// Optionally show a "refreshing" message
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Refreshing cards...</p>" cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
// Then reload the cards with the updated cache data
await loadCards(minterCardIdentifierPrefix) await loadCards(minterCardIdentifierPrefix)
}) })
document.getElementById("cancel-publish-button").addEventListener("click", async () => { document.getElementById("cancel-publish-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
@ -144,10 +210,48 @@ const loadMinterBoardPage = async () => {
await loadCards(minterCardIdentifierPrefix) await loadCards(minterCardIdentifierPrefix)
}) })
const showExistingCardsCheckbox = document.getElementById('show-existing-checkbox')
if (showExistingCardsCheckbox) {
showExistingCardsCheckbox.addEventListener('change', async (event) => {
await loadCards(minterCardIdentifierPrefix)
})
}
//Initialize Minter Group and Admin Group
await initializeCachedGroups()
await featureTriggerCheck() await featureTriggerCheck()
await loadCards(minterCardIdentifierPrefix) await loadCards(minterCardIdentifierPrefix)
} }
const initializeCachedGroups = async () => {
try {
const [minterGroup, minterAdmins] = await Promise.all([
fetchMinterGroupMembers(),
fetchMinterGroupAdmins()
])
cachedMinterGroup = minterGroup
cachedMinterAdmins = minterAdmins
} catch (error) {
console.error("Error initializing cached groups:", error)
}
}
const runWithConcurrency = async (tasks, concurrency = 5) => {
const results = []
let index = 0
const workers = new Array(concurrency).fill(null).map(async () => {
while (index < tasks.length) {
const currentIndex = index++
const task = tasks[currentIndex]
results[currentIndex] = await task()
}
})
await Promise.all(workers)
return results
}
const extractMinterCardsMinterName = async (cardIdentifier) => { const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix // Ensure the identifier starts with the prefix
@ -364,182 +468,258 @@ const processARBoardCards = async (allValidCards) => {
//Main function to load the Minter Cards ---------------------------------------- //Main function to load the Minter Cards ----------------------------------------
const loadCards = async (cardIdentifierPrefix) => { const loadCards = async (cardIdentifierPrefix) => {
if ((!cachedMinterGroup || cachedMinterGroup.length === 0) || (!cachedMinterAdmins || cachedMinterAdmins.length === 0)) {
await initializeCachedGroups()
}
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
let isARBoard = false
cardsContainer.innerHTML = "<p>Loading cards...</p>" cardsContainer.innerHTML = "<p>Loading cards...</p>"
if (cardIdentifierPrefix.startsWith("QM-AR-card")) { const counterSpan = document.getElementById("board-card-counter")
isARBoard = true if (counterSpan) counterSpan.textContent = "(loading...)"
console.warn(`ARBoard determined:`, isARBoard)
} const isARBoard = cardIdentifierPrefix.startsWith("QM-AR-card")
const showExistingCheckbox = document.getElementById("show-existing-checkbox")
const showExisting = showExistingCheckbox && showExistingCheckbox.checked
let afterTime = 0 let afterTime = 0
const timeRangeSelect = document.getElementById("time-range-select") const timeRangeSelect = document.getElementById("time-range-select")
if (timeRangeSelect) { if (timeRangeSelect) {
const days = parseInt(timeRangeSelect.value, 10) const days = parseInt(timeRangeSelect.value, 10)
if (days > 0) { if (days > 0) {
const now = Date.now() const now = Date.now()
const dayMs = 24 * 60 * 60 * 1000 afterTime = now - days * 24 * 60 * 60 * 1000
afterTime = now - days * dayMs // e.g. last X days
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
} }
} }
try { try {
// 1) Fetch raw "BLOG_POST" entries const rawResults = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime) if (!rawResults || !Array.isArray(rawResults) || rawResults.length === 0) {
if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>" cardsContainer.innerHTML = "<p>No cards found.</p>"
return return
} }
// 2) Validate structure
const validatedCards = await Promise.all(
response.map(async (card) => {
const isValid = await validateCardStructure(card)
return isValid ? card : null
})
)
const validCards = validatedCards.filter((card) => card !== null)
if (validCards.length === 0) { const validated = (await Promise.all(
rawResults.map(async (r) => (await validateCardStructure(r)) ? r : null)
)).filter(Boolean)
if (validated.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>" cardsContainer.innerHTML = "<p>No valid cards found.</p>"
return return
} }
// Additional logic for ARBoard or MinterCards
const finalCards = isARBoard
? await processARBoardCards(validCards)
: await processMinterBoardCards(validCards)
// Sort finalCards according to selectedSort let processedCards
let selectedSort = 'newest' if (isARBoard) {
const sortSelect = document.getElementById('sort-select') processedCards = await processARBoardCards(validated)
} else {
processedCards = await processMinterBoardCards(validated)
}
let selectedSort = "newest"
const sortSelect = document.getElementById("sort-select")
if (sortSelect) { if (sortSelect) {
selectedSort = sortSelect.value selectedSort = sortSelect.value
} }
if (selectedSort === "name") {
if (selectedSort === 'name') { processedCards.sort((a, b) => (a.name||"").localeCompare(b.name||""))
finalCards.sort((a, b) => {
const nameA = a.name?.toLowerCase() || ''
const nameB = b.name?.toLowerCase() || ''
return nameA.localeCompare(nameB)
})
} else if (selectedSort === 'recent-comments') { } else if (selectedSort === 'recent-comments') {
// If you need the newest comment timestamp // If you need the newest comment timestamp
for (let card of finalCards) { for (let card of finalCards) {
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier) 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). finalCards.sort((a, b) =>
// Create the 'finalCardsArray' that includes the data, etc. (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
let finalCardsArray = [] )
cardsContainer.innerHTML = '' } else if (selectedSort === 'least-votes') {
for (const card of finalCards) { await applyVoteSortingData(finalCards, /* ascending= */ true)
try { } else if (selectedSort === 'most-votes') {
const skeletonHTML = createSkeletonCardHTML(card.identifier) await applyVoteSortingData(finalCards, /* ascending= */ false)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) }
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier
})
if (!cardDataResponse || !cardDataResponse.poll) { cardsContainer.innerHTML = "" // reset
// skip for (const card of processedCards) {
console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`) const skeletonHTML = createSkeletonCardHTML(card.identifier)
removeSkeleton(card.identifier) cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
continue }
const finalCardsArray = []
const alreadyMinterCards = []
const tasks = processedCards.map(card => {
return async () => {
// We'll store an object with skip info, QDN data, etc.
const result = {
card,
skip: false,
skipReason: "",
isAlreadyMinter: false,
cardData: null,
} }
// Extra validation: check poll ownership matches card publisher
const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll) try {
const cardPublisherAddress = await fetchOwnerAddressFromName(card.name) const data = await qortalRequest({
if (pollPublisherAddress !== cardPublisherAddress) { action: "FETCH_QDN_RESOURCE",
console.warn(`Poll hijack attack found, discarding card ${card.identifier}`) name: card.name,
removeSkeleton(card.identifier) service: "BLOG_POST",
continue identifier: card.identifier
} })
// If ARBoard, do a quick address check if (!data || !data.poll) {
if (isARBoard) { result.skip = true
const ok = await verifyMinter(cardDataResponse.minterName) result.skipReason = "Missing or invalid poll"
if (!ok) { return result
console.warn(`Card is not a minter nor an admin, not including in ARBoard. identifier: ${card.identifier}`)
removeSkeleton(card.identifier)
continue
} }
} else {
const isAlreadyMinter = await verifyMinter(cardDataResponse.creator) const pollPublisherAddress = await getPollOwnerAddressCached(data.poll)
if (isAlreadyMinter) { const cardPublisherAddress = await fetchOwnerAddressFromNameCached(card.name)
console.warn(`card IS ALREADY a minter, NOT displaying following identifier on the MinterBoard: ${card.identifier}`) if (pollPublisherAddress !== cardPublisherAddress) {
removeSkeleton(card.identifier) result.skip = true
continue result.skipReason = "Poll hijack mismatch"
return result
} }
// ARBoard => verify user is minter/admin
if (isARBoard) {
const ok = await verifyMinterCached(data.minterName)
if (!ok) {
result.skip = true
result.skipReason = "Card user not minter => skip from ARBoard"
return result
}
} else {
// MinterBoard => skip if user is minter
const isAlready = await verifyMinterCached(data.creator)
if (isAlready) {
result.skip = true
result.skipReason = "Already a minter"
result.isAlreadyMinter = true
result.cardData = data
return result
}
}
// If we get here => it's a keeper
result.cardData = data
} catch (err) {
console.warn("Error fetching resource or skip logic:", err)
result.skip = true
result.skipReason = "Error: " + err
} }
// **Push** to finalCardsArray for further processing (duplicates, etc.)
return result
}
})
// ADJUST THE CONCURRENCY TO INCREASE THE AMOUNT OF CARDS PROCESSED AT ONCE. INCREASE UNTIL THERE ARE ISSUES.
const concurrency = 30
const results = await runWithConcurrency(tasks, concurrency)
// Fill final arrays
for (const r of results) {
if (r.skip && r.isAlreadyMinter) {
alreadyMinterCards.push({ ...r.card, cardDataResponse: r.cardData })
removeSkeleton(r.card.identifier)
} else if (r.skip) {
console.warn(`Skipping card ${r.card.identifier}, reason=${r.skipReason}`)
removeSkeleton(r.card.identifier)
} else {
// keeper
finalCardsArray.push({ finalCardsArray.push({
...card, ...r.card,
cardDataResponse, cardDataResponse: r.cardData
pollPublisherAddress,
cardPublisherAddress,
}) })
} catch (err) {
console.error(`Error preparing card ${card.identifier}`, err)
removeSkeleton(card.identifier)
} }
} }
// Next, do the actual rendering:
// cardsContainer.innerHTML = ""
for (const cardObj of finalCardsArray) { for (const cardObj of finalCardsArray) {
// Insert a skeleton first if you like try {
// const skeletonHTML = createSkeletonCardHTML(cardObj.identifier) const pollResults = await fetchPollResultsCached(cardObj.cardDataResponse.poll)
// cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) const commentCount = await countCommentsCached(cardObj.identifier)
// Build final HTML const cardUpdatedTime = cardObj.updated || cardObj.created || null
const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll) const bgColor = generateDarkPastelBackgroundBy(cardObj.name)
const commentCount = await countComments(cardObj.identifier)
const cardUpdatedTime = cardObj.updated || null
const bgColor = generateDarkPastelBackgroundBy(cardObj.name)
// Construct the final HTML for each card
const finalCardHTML = isARBoard
? await createARCardHTML(
cardObj.cardDataResponse,
pollResults,
cardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
cardObj.cardPublisherAddress,
cardObj.isDuplicate
)
: await createCardHTML(
cardObj.cardDataResponse,
pollResults,
cardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
cardObj.cardPublisherAddress
)
replaceSkeleton(cardObj.identifier, finalCardHTML) // If ARBoard => createARCardHTML else createCardHTML
const finalCardHTML = isARBoard
? await createARCardHTML(
cardObj.cardDataResponse,
pollResults,
cardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
await fetchOwnerAddressFromNameCached(cardObj.name),
cardObj.isDuplicate
)
: await createCardHTML(
cardObj.cardDataResponse,
pollResults,
cardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
await fetchOwnerAddressFromNameCached(cardObj.name)
)
replaceSkeleton(cardObj.identifier, finalCardHTML)
} catch (err) {
console.error(`Error finalizing card ${cardObj.identifier}:`, err)
removeSkeleton(cardObj.identifier)
}
}
if (showExisting && alreadyMinterCards.length > 0) {
console.log(`Rendering minted cards because showExisting is checked, count=${alreadyMinterCards.length}`)
for (const minted of alreadyMinterCards) {
const skeletonHTML = createSkeletonCardHTML(minted.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
try {
const pollResults = await fetchPollResultsCached(minted.cardDataResponse.poll)
const commentCount = await countCommentsCached(minted.identifier)
const cardUpdatedTime = minted.updated || minted.created || null
const bgColor = generateDarkPastelBackgroundBy(minted.name)
const finalCardHTML = await createCardHTML(
minted.cardDataResponse,
pollResults,
minted.identifier,
commentCount,
cardUpdatedTime,
bgColor,
await fetchOwnerAddressFromNameCached(minted.name),
/* isExistingMinter= */ true
)
replaceSkeleton(minted.identifier, finalCardHTML)
} catch (err) {
console.error(`Error finalizing minted card ${minted.identifier}:`, err)
removeSkeleton(minted.identifier)
}
}
}
if (counterSpan) {
const displayed = finalCardsArray.length
const minted = alreadyMinterCards.length
counterSpan.textContent = `(${displayed} displayed, ${minted} minters)`
} }
} catch (error) { } catch (error) {
console.error("Error loading cards:", error) console.error("Error loading cards:", error)
cardsContainer.innerHTML = "<p>Failed to load cards.</p>" cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
if (counterSpan) {
counterSpan.textContent = "(error loading)"
}
} }
} }
const verifyMinterCache = new Map()
const verifyMinterCached = async (nameOrAddress) => {
if (verifyMinterCache.has(nameOrAddress)) {
return verifyMinterCache.get(nameOrAddress)
}
const result = await verifyMinter(nameOrAddress)
verifyMinterCache.set(nameOrAddress, result)
return result
}
const verifyMinter = async (minterName) => { const verifyMinter = async (minterName) => {
try { try {
const nameInfo = await getNameInfo(minterName) const nameInfo = await getNameInfoCached(minterName)
if (!nameInfo) return false if (!nameInfo) return false
const minterAddress = nameInfo.owner const minterAddress = nameInfo.owner
@ -547,8 +727,10 @@ const verifyMinter = async (minterName) => {
if (!isValid) return false if (!isValid) return false
// Then check if they're in the minter group // Then check if they're in the minter group
const minterGroup = await fetchMinterGroupMembers() // const minterGroup = await fetchMinterGroupMembers()
const adminGroup = await fetchMinterGroupAdmins() const minterGroup = cachedMinterGroup
// const adminGroup = await fetchMinterGroupAdmins()
const adminGroup = cachedMinterAdmins
const minterGroupAddresses = minterGroup.map(m => m.member) const minterGroupAddresses = minterGroup.map(m => m.member)
const adminGroupAddresses = adminGroup.map(m => m.member) const adminGroupAddresses = adminGroup.map(m => m.member)
@ -561,8 +743,10 @@ const verifyMinter = async (minterName) => {
} }
const applyVoteSortingData = async (cards, ascending = true) => { const applyVoteSortingData = async (cards, ascending = true) => {
const minterGroupMembers = await fetchMinterGroupMembers() // const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins() const minterGroupMembers = cachedMinterGroup
// const minterAdmins = await fetchMinterGroupAdmins()
const minterAdmins = cachedMinterAdmins
for (const card of cards) { for (const card of cards) {
try { try {
@ -579,7 +763,7 @@ const applyVoteSortingData = async (cards, ascending = true) => {
card._minterYes = 0 card._minterYes = 0
continue continue
} }
const pollResults = await fetchPollResults(cardDataResponse.poll); const pollResults = await fetchPollResultsCached(cardDataResponse.poll);
const { adminYes, adminNo, minterYes, minterNo } = await processPollData( const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults, pollResults,
minterGroupMembers, minterGroupMembers,
@ -750,8 +934,8 @@ const loadCardIntoForm = async (cardData) => {
// Main function to publish a new Minter Card ----------------------------------------------- // Main function to publish a new Minter Card -----------------------------------------------
const publishCard = async (cardIdentifierPrefix) => { const publishCard = async (cardIdentifierPrefix) => {
// const minterGroupData = await fetchMinterGroupMembers()
const minterGroupData = await fetchMinterGroupMembers() const minterGroupData = cachedMinterGroup
const minterGroupAddresses = minterGroupData.map(m => m.member) const minterGroupAddresses = minterGroupData.map(m => m.member)
const userAddress = userState.accountAddress const userAddress = userState.accountAddress
@ -759,6 +943,7 @@ const publishCard = async (cardIdentifierPrefix) => {
alert("You are already a Minter and cannot publish a new card!") alert("You are already a Minter and cannot publish a new card!")
return return
} }
const header = document.getElementById("card-header").value.trim() const header = document.getElementById("card-header").value.trim()
const content = document.getElementById("card-content").value.trim() const content = document.getElementById("card-content").value.trim()
const links = Array.from(document.querySelectorAll(".card-link")) const links = Array.from(document.querySelectorAll(".card-link"))
@ -770,8 +955,27 @@ const publishCard = async (cardIdentifierPrefix) => {
return return
} }
const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}` if (isExistingCard) {
const pollName = `${cardIdentifier}-poll` if (!existingCardData || Object.keys(existingCardData).length === 0) {
const fetched = await fetchExistingCard(cardIdentifierPrefix)
if (fetched) {
existingCardData = fetched
} else {
console.warn("fetchExistingCard returned null. Possibly no existing card found.")
}
}
}
const cardIdentifier = isExistingCard && existingCardIdentifier
? existingCardIdentifier
: `${cardIdentifierPrefix}-${await uid()}`
let existingPollName
if (existingCardData && existingCardData.poll) {
existingPollName = existingCardData.poll
}
const pollName = existingPollName || `${cardIdentifier}-poll`
const pollDescription = `Mintership Board Poll for ${userState.accountName}` const pollDescription = `Mintership Board Poll for ${userState.accountName}`
const cardData = { const cardData = {
@ -781,16 +985,16 @@ const publishCard = async (cardIdentifierPrefix) => {
creator: userState.accountName, creator: userState.accountName,
creatorAddress: userState.accountAddress, creatorAddress: userState.accountAddress,
timestamp: Date.now(), timestamp: Date.now(),
poll: pollName, poll: pollName // either the existing poll or a new one
} }
try { try {
let base64CardData = await objectToBase64(cardData) let base64CardData = await objectToBase64(cardData)
if (!base64CardData) { if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`) console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
base64CardData = btoa(JSON.stringify(cardData)) base64CardData = btoa(JSON.stringify(cardData))
} }
await qortalRequest({ await qortalRequest({
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, name: userState.accountName,
@ -799,7 +1003,7 @@ const publishCard = async (cardIdentifierPrefix) => {
data64: base64CardData, data64: base64CardData,
}) })
if (!isExistingCard){ if (!isExistingCard || !existingPollName) {
await qortalRequest({ await qortalRequest({
action: "CREATE_POLL", action: "CREATE_POLL",
pollName, pollName,
@ -807,26 +1011,33 @@ const publishCard = async (cardIdentifierPrefix) => {
pollOptions: ['Yes, No'], pollOptions: ['Yes, No'],
pollOwnerAddress: userState.accountAddress, pollOwnerAddress: userState.accountAddress,
}) })
alert("Card and poll published successfully!") if (!isExistingCard) {
alert("Card and poll published successfully!")
} else {
alert("Existing card updated, and new poll created (since existing poll was missing)!")
}
} else {
alert("Card updated successfully! (No poll updates possible)")
} }
if (isExistingCard){ if (isExistingCard) {
alert("Card Updated Successfully! (No poll updates possible)")
isExistingCard = false isExistingCard = false
existingCardData = {}
} }
document.getElementById("publish-card-form").reset() document.getElementById("publish-card-form").reset()
document.getElementById("publish-card-view").style.display = "none" document.getElementById("publish-card-view").style.display = "none"
document.getElementById("cards-container").style.display = "flex" document.getElementById("cards-container").style.display = "flex"
await loadCards(minterCardIdentifierPrefix) await loadCards(minterCardIdentifierPrefix)
} catch (error) { } catch (error) {
console.error("Error publishing card or poll:", error) console.error("Error publishing card or poll:", error)
alert("Failed to publish card and poll.") alert("Failed to publish card and poll.")
} }
} }
let globalVoterMap = new Map() let globalVoterMap = new Map()
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => { const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => {
@ -1193,6 +1404,7 @@ const toggleComments = async (cardIdentifier) => {
const commentButton = document.getElementById(`comment-button-${cardIdentifier}`) const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return if (!commentsSection || !commentButton) return
const count = commentButton.dataset.commentCount const count = commentButton.dataset.commentCount
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display) const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display)
@ -1210,6 +1422,16 @@ const toggleComments = async (cardIdentifier) => {
} }
} }
const commentCountCache = new Map()
const countCommentsCached= async (cardIdentifier) => {
if (commentCountCache.has(cardIdentifier)) {
return commentCountCache.get(cardIdentifier)
}
const count = await countComments(cardIdentifier)
commentCountCache.set(cardIdentifier, count)
return count
}
const countComments = async (cardIdentifier) => { const countComments = async (cardIdentifier) => {
try { try {
const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false') const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false')
@ -1360,7 +1582,7 @@ const handleInviteMinter = async (minterName) => {
try { try {
const blockInfo = await getLatestBlockInfo() const blockInfo = await getLatestBlockInfo()
const blockHeight = blockInfo.height const blockHeight = blockInfo.height
const minterAccountInfo = await getNameInfo(minterName) const minterAccountInfo = await getNameInfoCached(minterName)
const minterAddress = await minterAccountInfo.owner const minterAddress = await minterAccountInfo.owner
let adminPublicKey let adminPublicKey
let txGroupId let txGroupId
@ -1444,7 +1666,8 @@ const featureTriggerCheck = async () => {
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()
const minterAdmins = await fetchMinterGroupAdmins() // const minterAdmins = await fetchMinterGroupAdmins()
const minterAdmins = cachedMinterAdmins
// default needed admin count = 9, or 40% if block has passed // default needed admin count = 9, or 40% if block has passed
let minAdminCount = 9 let minAdminCount = 9
@ -1460,7 +1683,7 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
} }
console.log(`passed initial button creation checks (adminYes >= ${minAdminCount})`) console.log(`passed initial button creation checks (adminYes >= ${minAdminCount})`)
// get user's address from 'creator' name // get user's address from 'creator' name
const minterNameInfo = await getNameInfo(creator) const minterNameInfo = await getNameInfoCached(creator)
if (!minterNameInfo || !minterNameInfo.owner) { if (!minterNameInfo || !minterNameInfo.owner) {
console.warn(`No valid nameInfo for ${creator}, skipping invite button.`) console.warn(`No valid nameInfo for ${creator}, skipping invite button.`)
return null return null
@ -1469,7 +1692,7 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
// fetch all final KICK/BAN tx // fetch all final KICK/BAN tx
const { finalKickTxs, finalBanTxs } = await fetchAllKickBanTxData() const { finalKickTxs, finalBanTxs } = await fetchAllKickBanTxData()
const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions() const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions()
// check if there's a final (non-pending) KICK or BAN for this user // check if there's a KICK or BAN for this user.
const priorKick = finalKickTxs.some(tx => tx.member === minterAddress) const priorKick = finalKickTxs.some(tx => tx.member === minterAddress)
const priorBan = finalBanTxs.some(tx => tx.offender === minterAddress) const priorBan = finalBanTxs.some(tx => tx.offender === minterAddress)
const existingInvite = finalInviteTxs.some(tx => tx.invitee === minterAddress) const existingInvite = finalInviteTxs.some(tx => tx.invitee === minterAddress)
@ -1480,10 +1703,12 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
// build the normal invite button & groupApprovalHtml // build the normal invite button & groupApprovalHtml
let inviteButtonHtml = "" let inviteButtonHtml = ""
if (existingInvite || pendingInvite){ if (existingInvite || pendingInvite){
console.warn(`There is an EXISTING INVITE for this user! No invite button being created... existing: (${existingInvite}, pending: ${pendingInvite})`) console.warn(`There is an EXISTING or PENDING INVITE for this user! No invite button being created... existing: (${existingInvite}, pending: ${pendingInvite})`)
inviteButtonHtml = '' inviteButtonHtml = ''
} else {
inviteButtonHtml = isSomeTypaAdmin ? createInviteButtonHtml(creator, cardIdentifier) : ""
} }
inviteButtonHtml = isSomeTypaAdmin ? createInviteButtonHtml(creator, cardIdentifier) : ""
const groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE") const groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE")
// if user had no prior KICK/BAN // if user had no prior KICK/BAN
@ -1507,10 +1732,8 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
} }
} }
const findPendingApprovalTxForAddress = async (address, txType, limit = 0, offset = 0) => { const findPendingTxForAddress = async (address, txType, limit = 0, offset = 0) => {
// 1) Fetch all pending transactions
const pendingTxs = await searchPendingTransactions(limit, offset, false) const pendingTxs = await searchPendingTransactions(limit, offset, false)
// if a txType is passed, return the results related to that type, if not, then return any pending tx of the potential types.
let relevantTypes let relevantTypes
if (txType) { if (txType) {
relevantTypes = new Set([txType]) relevantTypes = new Set([txType])
@ -1547,7 +1770,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
// We are going to be verifying that the address isn't already a minter, before showing GROUP_APPROVAL buttons potentially... // We are going to be verifying that the address isn't already a minter, before showing GROUP_APPROVAL buttons potentially...
if (transactionType === "GROUP_INVITE") { if (transactionType === "GROUP_INVITE") {
console.log(`This is a GROUP_INVITE check for group approval... Checking that user isn't already a minter...`) console.log(`This is a GROUP_INVITE check for group approval... Checking that user isn't already a minter...`)
const minterMembers = await fetchMinterGroupMembers() // const minterMembers = await fetchMinterGroupMembers()
const minterMembers = cachedMinterGroup
const minterGroupAddresses = minterMembers.map(m => m.member) const minterGroupAddresses = minterMembers.map(m => m.member)
if (minterGroupAddresses.includes(address)) { if (minterGroupAddresses.includes(address)) {
console.warn(`User is already a minter, will not be creating group_approval buttons`) console.warn(`User is already a minter, will not be creating group_approval buttons`)
@ -1565,15 +1789,15 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
blockLimit: 0, blockLimit: 0,
txGroupId: 0 txGroupId: 0
}) })
const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType, 0, 0) const pendingTxs = await findPendingTxForAddress(address, transactionType, 0, 0)
let isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin let isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
// If no pending transaction found, return null // If no pending transaction found, return null
if (!pendingApprovals || pendingApprovals.length === 0) { if (!pendingTxs || pendingTxs.length === 0) {
console.warn("no pending approval transactions found, returning null...") console.warn("no pending transactions found, returning null...")
return null return null
} }
const txSig = pendingApprovals[0].signature const txSig = pendingTxs[0].signature
// Find the relevant signature. (First approval) // Find the relevant signature. (signature of the issued transaction pending.)
const relevantApprovals = approvalSearchResults.filter( const relevantApprovals = approvalSearchResults.filter(
(approvalTx) => approvalTx.pendingSignature === txSig (approvalTx) => approvalTx.pendingSignature === txSig
) )
@ -1903,7 +2127,7 @@ const getNewestCommentTimestamp = async (cardIdentifier) => {
} }
// Create the overall Minter Card HTML ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address) => { const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address, isExistingMinter=false) => {
const { header, content, links, creator, creatorAddress, timestamp, poll } = cardData const { header, content, links, creator, creatorAddress, timestamp, poll } = cardData
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString() const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
const avatarHtml = await getMinterAvatar(creator) const avatarHtml = await getMinterAvatar(creator)
@ -1913,13 +2137,15 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
</button> </button>
`).join("") `).join("")
const minterGroupMembers = await fetchMinterGroupMembers() // const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins() const minterGroupMembers = cachedMinterGroup
// const minterAdmins = await fetchMinterGroupAdmins()
const minterAdmins = cachedMinterAdmins
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier) const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
createModal('links') createModal('links')
createModal('poll-details') createModal('poll-details')
const inviteButtonHtml = await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier) const inviteButtonHtml = isExistingMinter ? "" : await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier)
let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : '' let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : ''
let finalBgColor = bgColor let finalBgColor = bgColor
@ -1935,6 +2161,9 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
finalBgColor = "rgba(1, 65, 39, 0.41)"; // or any green you want finalBgColor = "rgba(1, 65, 39, 0.41)"; // or any green you want
} else if (userVote === 1) { } else if (userVote === 1) {
finalBgColor = "rgba(107, 3, 3, 0.3)"; // or any red you want finalBgColor = "rgba(107, 3, 3, 0.3)"; // or any red you want
} else if (isExistingMinter){
finalBgColor = "rgb(99, 99, 99)"
invitedText = `<h4 style="color:rgb(135, 55, 16); margin-bottom: 0.5em;">EXISTING MINTER</h4>`
} else if (hasMinterInvite) { } else if (hasMinterInvite) {
// If so, override background color & add an "INVITED" label // If so, override background color & add an "INVITED" label
finalBgColor = "black"; finalBgColor = "black";

View File

@ -1,4 +1,4 @@
const Q_MINTERSHIP_VERSION = "1.06" const Q_MINTERSHIP_VERSION = "1.22"
const messageIdentifierPrefix = `mintership-forum-message` const messageIdentifierPrefix = `mintership-forum-message`
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment` const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`

View File

@ -6,6 +6,11 @@ let baseUrl = ''
let isOutsideOfUiDevelopment = false let isOutsideOfUiDevelopment = false
let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG' let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG'
// Caching to improve performance
const nameInfoCache = new Map(); // name -> nameInfo
const addressInfoCache = new Map(); // address -> addressInfo
const pollResultsCache = new Map(); // pollName -> pollResults
if (typeof qortalRequest === 'function') { if (typeof qortalRequest === 'function') {
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.') console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
isOutsideOfUiDevelopment = false isOutsideOfUiDevelopment = false
@ -223,7 +228,20 @@ const getUserAddress = async () => {
} }
} }
const getAddressInfoCached = async (address) => {
if (addressInfoCache.has(address)) return addressInfoCache.get(address)
const result = await getAddressInfo(address)
addressInfoCache.set(address, result)
return result
}
const getAddressInfo = async (address) => { const getAddressInfo = async (address) => {
const qortalAddressPattern = /^Q[A-Za-z0-9]{33}$/ // Q + 33 almum = 34 total length
if (!qortalAddressPattern.test(address)) {
console.warn(`Not a valid Qortal address format, returning same thing that was passed to not break other functions: ${address}`)
return address
}
try { try {
const response = await fetch (`${baseUrl}/addresses/${address}`, { const response = await fetch (`${baseUrl}/addresses/${address}`, {
headers: { 'Accept': 'application/json' }, headers: { 'Accept': 'application/json' },
@ -248,6 +266,19 @@ const getAddressInfo = async (address) => {
} }
} }
const nameToAddressCache = new Map()
const fetchOwnerAddressFromNameCached = async (name) => {
if (nameToAddressCache.has(name)) {
return nameToAddressCache.get(name)
}
const address = await fetchOwnerAddressFromName(name)
nameToAddressCache.set(name, address)
return address
}
const fetchOwnerAddressFromName = async (name) => { const fetchOwnerAddressFromName = async (name) => {
console.log('fetchOwnerAddressFromName called') console.log('fetchOwnerAddressFromName called')
console.log('name:', name) console.log('name:', name)
@ -326,6 +357,15 @@ const verifyAddressIsAdmin = async (address) => {
throw error throw error
} }
} }
const getNameInfoCached = async (name) => {
if (nameInfoCache.has(name)) {
return nameInfoCache.get(name)
}
const result = await getNameInfo(name)
nameInfoCache.set(name, result)
return result
}
const getNameInfo = async (name) => { const getNameInfo = async (name) => {
console.log('getNameInfo called') console.log('getNameInfo called')
@ -786,18 +826,20 @@ const searchSimple = async (service, identifier, name, limit=1500, offset=0, roo
if (name && !identifier && !room) { if (name && !identifier && !room) {
console.log('name only searchSimple', name) console.log('name only searchSimple', name)
urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}` urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}&after=${after}`
console.log(`urlSuffix used: ${urlSuffix}`)
} else if (!name && identifier && !room) { } else if (!name && identifier && !room) {
console.log('identifier only searchSimple', identifier) console.log('identifier only searchSimple', identifier)
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}` urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}&after=${after}`
console.log(`urlSuffix used: ${urlSuffix}`)
} else if (!name && !identifier && !room) { } else if (!name && !identifier && !room) {
console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`) console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`)
return null return null
} else { } else {
console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}'`) console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}', after: ${after}`)
} }
const response = await fetch(`${baseUrl}/arbitrary/resources/searchsimple?${urlSuffix}`, { const response = await fetch(`${baseUrl}/arbitrary/resources/searchsimple?${urlSuffix}`, {
@ -1231,6 +1273,20 @@ const getProductDetails = async (service, name, identifier) => {
// Qortal poll-related calls ---------------------------------------------------------------------- // Qortal poll-related calls ----------------------------------------------------------------------
const pollOwnerAddrCache = new Map()
const getPollOwnerAddressCached = async (pollName) => {
if (pollOwnerAddrCache.has(pollName)) {
return pollOwnerAddrCache.get(pollName)
}
const ownerAddress = await getPollOwnerAddress(pollName)
// Store in cache
pollOwnerAddrCache.set(pollName, ownerAddress)
return ownerAddress
}
const getPollOwnerAddress = async (pollName) => { const getPollOwnerAddress = async (pollName) => {
try { try {
const response = await fetch(`${baseUrl}/polls/${pollName}`, { const response = await fetch(`${baseUrl}/polls/${pollName}`, {
@ -1259,6 +1315,15 @@ const getPollPublisherPublicKey = async (pollName) => {
} }
} }
const fetchPollResultsCached = async (pollName) => {
if (pollResultsCache.has(pollName)) {
return pollResultsCache.get(pollName)
}
const result = await fetchPollResults(pollName)
pollResultsCache.set(pollName, result)
return result
}
const fetchPollResults = async (pollName) => { const fetchPollResults = async (pollName) => {
try { try {
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, { const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
@ -1346,7 +1411,7 @@ const processTransaction = async (signedTransaction) => {
// Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days. // Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days.
// We will also default to the MINTER group for groupId, AFTER the GROUP_APPROVAL changes, the txGroupId will need to be set for tx that require approval. // We will also default to the MINTER group for groupId, AFTER the GROUP_APPROVAL changes, the txGroupId will need to be set for tx that require approval.
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive, txGroupId, fee) => { const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive=0, txGroupId, fee) => {
try { try {
// Fetch account reference correctly // Fetch account reference correctly
@ -1395,16 +1460,16 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr
} }
} }
const createGroupKickTransaction = async (recipientAddress, adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId, fee) => { const createGroupKickTransaction = async (adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId=694, fee=0.01) => {
try { try {
// Fetch account reference correctly // Fetch account reference correctly
const accountInfo = await getAddressInfo(recipientAddress) const accountInfo = await getAddressInfo(member)
const accountReference = accountInfo.reference const accountReference = accountInfo.reference
// Validate inputs before making the request // Validate inputs before making the request
if (!adminPublicKey || !accountReference || !recipientAddress) { if (!adminPublicKey || !accountReference || !member) {
throw new Error("Missing required parameters for group invite transaction.") throw new Error("Missing required parameters for group kick transaction.")
} }
const payload = { const payload = {
@ -1412,11 +1477,10 @@ const createGroupKickTransaction = async (recipientAddress, adminPublicKey, grou
reference: accountReference, reference: accountReference,
fee, fee,
txGroupId, txGroupId,
recipient: null,
adminPublicKey, adminPublicKey,
groupId: groupId, groupId,
member: member || recipientAddress, member,
reason: reason reason
} }
console.log("Sending GROUP_KICK transaction payload:", payload) console.log("Sending GROUP_KICK transaction payload:", payload)

View File

@ -196,13 +196,25 @@ const fetchAllInviteTransactions = async () => {
const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx) const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx)
console.log('Final kickTxs:', finalInviteTxs) console.log('Final InviteTxs:', finalInviteTxs)
console.log('Pending kickTxs:', pendingInviteTxs) console.log('Pending InviteTxs:', pendingInviteTxs)
return { return {
finalInviteTxs, finalInviteTxs,
pendingInviteTxs, pendingInviteTxs,
} }
} }
const findPendingApprovalsForTxSignature = async (txSignature, txType='GROUP_APPROVAL', limit=0, offset=0) => {
const pendingTxs = await searchPendingTransactions(limit, offset)
// Filter only the relevant GROUP_APPROVAL TX referencing txSignature
const approvals = pendingTxs.filter(tx =>
tx.type === txType && tx.pendingSignature === txSignature
)
console.log(`approvals found:`,approvals)
return approvals
}

View File

@ -141,7 +141,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">
<div class="card-wrapper" style="justify-content:center; align: center; align-text: center;"> <div class="card-wrapper" style="justify-content:center;">
<div class="icon-wrapper"> <div class="icon-wrapper">
<span class="mbr-iconfont mbr-iconfont-btn mbri-file" style="color:aliceblue;"></span> <span class="mbr-iconfont mbr-iconfont-btn mbri-file" style="color:aliceblue;"></span>
</div> </div>
@ -200,14 +200,14 @@
<div class="row"> <div class="row">
<div class="col-12 col-lg-7 card"> <div class="col-12 col-lg-7 card">
<div class="title-wrapper"> <div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2"> <h2 class="mbr-section-title mbr-fonts-style display-2 version">
v1.06beta 01-31-2025</h2> </h2>
</div> </div>
</div> </div>
<div class="col-12 col-lg-5 card"> <div class="col-12 col-lg-5 card">
<div class="text-wrapper"> <div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7"> <p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.06b Fixes</u></b>- <b>EMERGENCY UPDATE </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES, This is an emergency update that is meant to prevent the issue that took place yesterday and ended up stalling quite a few nodes. This means that Q-Mintership should be the ONLY APP UTILIZED FOR THE FUNCTIONALITY IT PROVIDES. <!-- <b class="version"><u>v1.06.4b</u></b>- <b>various improvements</b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES. -->
</p> </p>
</div> </div>
</div> </div>