Compare commits
15 Commits
testing-20
...
main
Author | SHA1 | Date | |
---|---|---|---|
731c53b5d4 | |||
20f9845610 | |||
5b49f0d4fc | |||
b2dde1ea56 | |||
5630f80a54 | |||
59bd5cc760 | |||
6f459d7e0a | |||
fe230a91d3 | |||
5443d159b0 | |||
e0c5a09378 | |||
509e3bf357 | |||
07f4fa3e6e | |||
7afa06623f | |||
51921992e2 | |||
f5ce634ff5 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
/.vscode
|
|
||||||
/.sync*
|
|
Binary file not shown.
Binary file not shown.
49
README.md
Normal file
49
README.md
Normal 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.
|
@ -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 */
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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`
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user