Compare commits

...

19 Commits

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

2
.gitignore vendored
View File

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

Binary file not shown.

Binary file not shown.

49
README.md Normal file
View File

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

View File

@ -461,6 +461,27 @@
/* General Page Styles */
/* Main Container for Minter Board */
input[type="checkbox"] {
width: 25px; /* Adjust width */
height: 25px; /* Adjust height */
appearance: none; /* Remove default styling */
background-color: rgb(15, 21, 22);
border: 2px solid rgb(98, 99, 106);
border-radius: 4px; /* Optional rounded corners */
display: inline-block;
position: relative;
}
input[type="checkbox"]:checked::after {
content: "✔"; /* Custom checkmark */
font-size: 20px;
color: rgb(201, 201, 201);
position: absolute;
top: 0;
left: 4px;
}
body {
background-color: black;
}
@ -555,6 +576,75 @@ body {
margin-bottom: 2vh;
}
.blocklist-form {
display: flex;
flex-direction: column;
background-color: #1b1b1b; /* or your dark color */
border: 1px solid #444;
border-radius: 5px;
padding: 20px;
margin: 20px auto; /* center horizontally */
/* max-width: 600px; */
color: #ddd; /* text color */
text-align: center;
align-items: center;
}
.blocklist-display {
/* This holds the list of blocked names */
border: 1px dashed;
background-color:#000000;
width: 90%;
font-size: 1.8rem;
color: #fff3f3;
text-align: center;
align-items: center;
/* you could style the list items or bullet if you like */
}
.blocklist-button-container {
/* This area will hold the text input + the row for buttons */
display: flex;
flex-direction: row;
gap: 10px;
}
.blocklist-form input.blocklist-input {
padding: 1rem;
font-size: 2rem;
line-height: 2;
background-color: #14161a;
border: 1px solid #8caeb0;
border-radius: 4px;
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 {
background-color: #529d8d84;
color: #fff;
border: none;
border-radius: 5px;
padding: 10px 14px;
cursor: pointer;
font-size: 1.5rem;
transition: background-color 0.2s ease;
}
.publish-card-button:hover {
background-color: #3b5c71; /* a darker variant */
}
.publish-card-view textarea {
min-height: 15vh;
resize: vertical;
@ -570,9 +660,9 @@ body {
margin-bottom: 1.5vh;
}
/* Buttons Inside Form */
#publish-card-form button {
background-color: #76c7c0;
/* Generic: all buttons inside .publish-card-form */
.publish-card-form button {
background-color: #359f4a;
color: #1e1e1e;
border: none;
border-radius: 0.5vh;
@ -583,30 +673,88 @@ body {
transition: background-color 0.3s;
}
#publish-card-form button:hover {
.publish-card-form button:hover {
background-color: #5e92a8;
}
#publish-card-form #add-link-button {
/* Then specifically override the add button */
.publish-card-form #blocklist-add-button {
background-color: #233748;
color: #ffffff;
}
.publish-card-form #blocklist-add-button:hover {
background-color: #1b1936;
}
.publish-card-form #add-link-button {
background-color: #233748;
color: #ffffff;
margin-bottom: 2vh;
}
#publish-card-form #add-link-button:hover {
.publish-card-form #add-link-button:hover {
background-color: #1b1936;
}
/* Cancel Button */
#publish-card-form #cancel-publish-button {
/* And specifically override the remove button */
.publish-card-form #blocklist-remove-button {
background-color: #463737;
color: #ffffff;
}
#publish-card-form #cancel-publish-button:hover {
.publish-card-form #blocklist-remove-button:hover {
background-color: #281e1e;
}
.publish-card-form #cancel-publish-button {
background-color: #463737;
color: #fff;
}
.publish-card-form #cancel-publish-button:hover {
background-color: #281e1e;
}
.approve-invite-list-button {
background-color: rgba(32, 88, 34, 0.554);
color: #fff;
border: none;
border-radius: 1vw;
padding: 1vh,2vh;
cursor: pointer;
font-size: 1.1rem;
transition: background-color 0.2s ease;
}
.approve-invite-list-button:hover {
background-color: rgba(34, 186, 47, 0.84); /* a darker variant */
}
.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 */
@media (max-width: 768px) {
.publish-card-view {
@ -619,7 +767,7 @@ body {
padding: 1.5vh;
}
#publish-card-form button {
.publish-card-form button {
font-size: 1.8vh;
padding: 1.2vh;
}
@ -628,8 +776,14 @@ body {
.refresh-cards-button {
border-color: white;
border-radius: 1.5vh;
background-color: black;
background-color: rgba(0, 0, 0, 0.089);
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 */

View File

@ -37,7 +37,7 @@ const loadAddRemoveAdminPage = async () => {
Propose a Minter for Admin Position
</button>
<div id="promotion-form-container" class="publish-card-view" style="display: none; margin-top: 1em;">
<form id="publish-card-form">
<form id="publish-card-form" class="publish-card-form">
<h3>Create or Update Promotion/Demotion Proposal Card</h3>
<label for="minter-name-input">Input NAME (promotion):</label>
<input type="text" id="minter-name-input" maxlength="100" placeholder="input NAME of MINTER for PROMOTION" required>
@ -59,12 +59,14 @@ const loadAddRemoveAdminPage = async () => {
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
<option value="0">All Creation Dates</option>
<option value="1">Last 1 Day</option>
<option value="7">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>
</div>
<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 () => {
const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
await initializeCachedGroups()
await loadCards(addRemoveIdentifierPrefix)
})
@ -117,6 +120,13 @@ const loadAddRemoveAdminPage = async () => {
linksContainer.appendChild(newLinkInput)
})
const timeRangeSelectCheckbox = document.getElementById('time-range-select')
if (timeRangeSelectCheckbox) {
timeRangeSelectCheckbox.addEventListener('change', async (event) => {
await loadCards(addRemoveIdentifierPrefix)
})
}
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault()
await publishARCard(addRemoveIdentifierPrefix)
@ -159,51 +169,63 @@ const fetchAllARTxData = async () => {
txGroupId: 694,
})
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs)
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs)
const { finalAddTxs, pendingAddTxs, expiredAddTxs } = partitionAddTransactions(allAddTxs)
const { finalRemTxs, pendingRemTxs, expiredRemTxs } = partitionRemoveTransactions(allRemTxs)
// We are going to keep all transactions in order to filter more accurately for display purposes.
console.log('Final addAdminTxs:', finalAddTxs);
console.log('Pending addAdminTxs:', pendingAddTxs);
console.log('Final remAdminTxs:', finalRemTxs);
console.log('Pending remAdminTxs:', pendingRemTxs);
console.log('Final addAdminTxs:', finalAddTxs)
console.log('Pending addAdminTxs:', pendingAddTxs)
console.log('expired addAdminTxs', expiredAddTxs)
console.log('Final remAdminTxs:', finalRemTxs)
console.log('Pending remAdminTxs:', pendingRemTxs)
console.log('expired remAdminTxs', expiredRemTxs)
return {
finalAddTxs,
pendingAddTxs,
expiredAddTxs,
finalRemTxs,
pendingRemTxs,
expiredRemTxs
}
}
function partitionAddTransactions(rawTransactions) {
const finalAddTxs = []
const pendingAddTxs = []
const partitionAddTransactions = (rawTransactions) => {
const finalAddTxs = []
const pendingAddTxs = []
const expiredAddTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingAddTxs.push(tx)
}
else if (tx.approvalStatus === 'EXPIRED'){
expiredAddTxs.push(tx)
} else {
finalAddTxs.push(tx)
}
}
return { finalAddTxs, pendingAddTxs, expiredAddTxs };
}
for (const tx of rawTransactions) {
const partitionRemoveTransactions = (rawTransactions) => {
const finalRemTxs = []
const pendingRemTxs = []
const expiredRemTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingAddTxs.push(tx)
} else {
finalAddTxs.push(tx)
}
}
return { finalAddTxs, pendingAddTxs };
}
function partitionRemoveTransactions(rawTransactions) {
const finalRemTxs = []
const pendingRemTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingRemTxs.push(tx)
} else {
}
else if (tx.approvalStatus === 'EXPIRED'){
expiredRemTxs.push(tx)
} else {
finalRemTxs.push(tx)
}
}
}
}
return { finalRemTxs, pendingRemTxs }
return { finalRemTxs, pendingRemTxs, expiredRemTxs }
}
@ -434,15 +456,17 @@ const publishARCard = async (cardIdentifierPrefix) => {
if (exists) {
alert(`An existing card was found, you must update it, two cards for the samme name cannot be published! Loading card data...`)
await loadCardIntoForm(existingCardData)
minterName = exists.minterName
const nameInfo = await getNameInfo(exists.minterName)
address = nameInfo.owner
isExistingCard = true
} else if (otherPublisher){
alert(`An existing card was found, but you are NOT the publisher, you may not publish duplicates, and you may not update a non-owned card! Please try again with another name, or use the existing card for ${minterNameInput}`)
return
}
if (exists.creator != userState.accountName) {
alert(`You are not the original publisher of this card, exiting.`)
return
}else {
await loadCardIntoForm(existingCardData)
minterName = exists.minterName
const nameInfo = await getNameInfo(exists.minterName)
address = nameInfo.owner
isExistingCard = true
}
}
const minterGroupData = await fetchMinterGroupMembers()
minterGroupAddresses = minterGroupData.map(m => m.member)
@ -481,6 +505,7 @@ const publishARCard = async (cardIdentifierPrefix) => {
const cardData = {
minterName,
minterAddress: address,
header,
content,
links,
@ -550,7 +575,7 @@ const checkAndDisplayActions = async (adminYes, name, cardIdentifier) => {
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
const totalAdmins = minterAdmins.length
const fortyPercent = totalAdmins * 0.40
minAdminCount = Math.round(fortyPercent)
minAdminCount = Math.ceil(fortyPercent)
console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`)
}
const addressInfo = await getNameInfo(name)
@ -735,7 +760,7 @@ const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins)
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => {
const { minterName, minterAddress='priorToAddition', header, content, links, creator, timestamp, poll, promotionCard } = cardData
const { minterName, minterAddress='', header, content, links, creator, timestamp, poll, promotionCard } = cardData
const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = await getMinterAvatar(minterName)
const creatorAvatar = await getMinterAvatar(creator)
@ -744,6 +769,14 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
${`Link ${index + 1} - ${link}`}
</button>
`).join("")
// Adding fix for accidental code in 1.04b
let publishedMinterAddress
if (!minterAddress || minterAddress ==='priorToAddition'){
publishedMinterAddress = ''
} else if (minterAddress){
console.log(`minter address found in card info: ${minterAddress}`)
publishedMinterAddress = minterAddress
}
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
@ -809,9 +842,9 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
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
)
const userPendingAdd = pendingAddTxs.some(
@ -823,31 +856,88 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
const userPendingRemove = pendingRemTxs.some(
(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 (confirmedAdd && !userPendingRemove && existingAdmin) {
if (userConfirmedAdd && !userPendingRemove && !userPendingAdd && noExpired && existingAdmin && promotionCard) {
console.warn(`account was already admin, final. no add/remove pending.`);
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 = ''
}
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`)
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 (confirmedRemove && !userPendingAdd && existingMinter) {
console.warn(`account was demoted, final. no add pending, existingMinter.`);
// If user has a final "remove" and no pending additions or removals and no expired transactions
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && noExpired && !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(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
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 (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...`)
// 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) {

View File

@ -10,8 +10,8 @@ let isTopic = false
let attemptLoadAdminDataCount = 0
let adminMemberCount = 0
let adminPublicKeys = []
let kickTransactions = []
let banTransactions = []
// let kickTransactions = []
// let banTransactions = []
let adminBoardState = {
kickedCards: new Set(), // store identifiers
bannedCards: new Set(), // likewise
@ -72,24 +72,25 @@ const loadAdminBoardPage = async () => {
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<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> 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>
<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>
<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>
<select id="sort-select" style="margin-left: 10px; padding: 5px;">
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(70, 106, 105); background-color: black;">
<option value="newest" selected>Sort by Date</option>
<option value="name">Sort by Name</option>
<option value="recent-comments">Newest Comments</option>
<option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option>
</select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
<option value="0">All Creation Dates</option>
<option value="1">Last 1 Day</option>
<option value="7">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>
<div class="show-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
@ -98,7 +99,7 @@ const loadAdminBoardPage = async () => {
</div>
<div id="encrypted-cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form">
<form id="publish-card-form" class="publish-card-form">
<h3>Create or Update an Admin Card</h3>
<div class="publish-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="topic-checkbox" name="topicMode" />
@ -195,69 +196,9 @@ const loadAdminBoardPage = async () => {
createScrollToTopButton()
// await fetchAndValidateAllAdminCards()
await updateOrSaveAdminGroupsDataLocally()
await fetchAllKicKBanTxData()
await fetchAllEncryptedCards()
}
const fetchAllKicKBanTxData = async () => {
const kickTxType = "GROUP_KICK"
const banTxType = "GROUP_BAN"
// Helper function to filter transactions
const filterTransactions = (rawTransactions) => {
// Group transactions by member
const memberTxMap = rawTransactions.reduce((map, tx) => {
if (!map[tx.member]) {
map[tx.member] = []
}
map[tx.member].push(tx)
return map
}, {})
// Filter out members with both pending and non-pending transactions
return Object.values(memberTxMap)
.filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING'))
.flat()
// .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
// txs.some(tx => tx.approvalStatus !== 'PENDING')))
// .flat()
}
// Fetch ban transactions
const rawBanTransactions = await searchTransactions({
txTypes: [banTxType],
address: '',
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
// Filter transactions for bans
banTransactions = filterTransactions(rawBanTransactions)
console.warn('banTxData (filtered):', banTransactions)
// Fetch kick transactions
const rawKickTransactions = await searchTransactions({
txTypes: [kickTxType],
address: '',
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
// Filter transactions for kicks
kickTransactions = filterTransactions(rawKickTransactions)
console.warn('kickTxData (filtered):', kickTransactions)
}
// Example: fetch and save admin public keys and count
const updateOrSaveAdminGroupsDataLocally = async () => {
@ -300,7 +241,7 @@ const loadOrFetchAdminGroupsData = async () => {
adminMemberCount = parsedData.keysCount
adminPublicKeys = parsedData.publicKeys
console.log(typeof adminPublicKeys); // Should be "object"
console.log(typeof adminPublicKeys) // Should be "object"
console.log(Array.isArray(adminPublicKeys))
console.log(`Loaded admins 'keysCount'=${adminMemberCount}, publicKeys=`, adminPublicKeys)
@ -744,16 +685,30 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
// If not topic mode, validate the user actually entered a valid Minter name
if (!isTopic) {
let minterAddress
publishedMinterName = await validateMinterName(minterNameInput)
if (!publishedMinterName) {
alert(`"${minterNameInput}" doesn't seem to be a valid name. Please check or use topic mode.`)
return
}
try {
const addressInfo = await getAddressInfo(minterNameInput)
if (addressInfo) {
console.warn(`checked minterNameInput and found it to be an address... proceeding accordingly.`)
minterAddress = addressInfo.address
publishedMinterName = addressInfo.address
} else {
alert(`"${minterNameInput}" doesn't seem to be a valid name or address. Please check or use topic mode.`)
return
}
} catch (error) {
console.warn(`error checking for address...?`, error)
alert(`Failed to verify name/address. Please try again, or change to topicMode to publish anything else.`)
return
}
}
// Also check for existing card if not topic
if (!isUpdateCard && existingCardMinterNames.some(item => item.minterName === publishedMinterName)) {
const duplicateCardData = existingCardMinterNames.find(item => item.minterName === publishedMinterName)
const updateCard = confirm(
`Minter Name: ${publishedMinterName} already has a card. Duplicate name-based cards are not allowed. You can OVERWRITE it or Cancel publishing. UPDATE CARD?`
`Minter Name: ${publishedMinterName} already has a card. (NOTE this update functionality is no longer functional, it may or may not come back. Even if you update the card you won't see it. It is suggested to CANCEL and use topic mode.`
)
if (updateCard) {
existingEncryptedCardIdentifier = duplicateCardData.identifier
@ -763,6 +718,9 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
}
}
}
if (!publishedMinterName && minterAddress){
console.log(`No name was found, but an address was, publishing address in cardData, and using address as name for card.`)
}
// Determine final card identifier
const currentTimestamp = Date.now()
@ -1067,7 +1025,7 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, name
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
const totalAdmins = minterAdmins.length
const fortyPercent = totalAdmins * 0.40
minAdminCount = Math.round(fortyPercent)
minAdminCount = Math.ceil(fortyPercent)
console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`)
}
if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
@ -1121,7 +1079,7 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
const handleKickMinter = async (minterName) => {
try {
isAddress = await getAddressInfo(minterName)
let isAddress = await getAddressInfo(minterName)
// Optional block check
let txGroupId = 0
@ -1134,7 +1092,7 @@ const handleKickMinter = async (minterName) => {
// Get the minter address from name info
let minterAddress
if (!isAddress){
if (!isAddress.address || !isAddress.address != minterName){
const minterNameInfo = await getNameInfo(minterName)
minterAddress = minterNameInfo?.owner
} else {
@ -1150,7 +1108,7 @@ const handleKickMinter = async (minterName) => {
const reason = 'Kicked by Minter Admins'
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({
action: "SIGN_TRANSACTION",
@ -1181,7 +1139,7 @@ const handleKickMinter = async (minterName) => {
}
const handleBanMinter = async (minterName) => {
isAddress = await getAddressInfo(minterName)
let isAddress = await getAddressInfo(minterName)
try {
let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo()
@ -1194,9 +1152,9 @@ const handleBanMinter = async (minterName) => {
txGroupId = 694
}
let minterAddress
if (!isAddress) {
if (!isAddress.address || !isAddress.address != minterName){
const minterNameInfo = await getNameInfo(minterName)
const minterAddress = minterNameInfo?.owner
minterAddress = minterNameInfo?.owner
} else {
minterAddress = minterName
}
@ -1205,7 +1163,6 @@ const handleBanMinter = async (minterName) => {
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
return
}
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const reason = 'Banned by Minter Admins'
const fee = 0.01
@ -1216,14 +1173,13 @@ const handleBanMinter = async (minterName) => {
action: "SIGN_TRANSACTION",
unsignedBytes: rawBanTransaction
})
if (!signedBanTransaction) {
console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`)
alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`)
return
}
let txToProcess = signedBanTransaction
const processedTx = await processTransaction(txToProcess)
if (typeof processedTx === 'object') {
@ -1260,7 +1216,7 @@ const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
// Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData
const { minterName, minterAddress = '', header, content, links, creator, timestamp, poll, topicMode } = cardData
const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null
const creatorAvatar = await getMinterAvatar(creator)
@ -1278,6 +1234,8 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
let showTopic = false
const { finalKickTxs, pendingKickTxs, finalBanTxs, pendingBanTxs } = await fetchAllKickBanTxData()
if (hasTopicMode) {
const modeVal = cardData.topicMode
showTopic = (modeVal === true || modeVal === 'true')
@ -1286,6 +1244,18 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
showTopic = false
}
}
let publishedMinterAddress = minterAddress
if (publishedMinterAddress === 'notYetAdded' || publishedMinterAddress === 'undefined' || publishedMinterAddress === null || !publishedMinterAddress) {
console.warn(`minterAddress is not published in the card data... will have to extract from minterName...`)
publishedMinterAddress = null
} else {
const publishedMinterAddressInfo = await getAddressInfo(publishedMinterAddress)
if (publishedMinterAddressInfo) {
console.log(`minterAddress found in published data, and verified. Using published address for further checks.`)
publishedMinterAddress = publishedMinterAddressInfo.address
}
}
let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
@ -1311,7 +1281,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
const verifiedName = await validateMinterName(minterName)
let levelText = '</h3>'
const addressVerification = await getAddressInfo(minterName)
const verifiedAddress = addressVerification.address
const verifiedAddress = publishedMinterAddress ? publishedMinterAddress : addressVerification.address
if (verifiedName || verifiedAddress) {
let accountInfo
@ -1319,36 +1289,41 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
accountInfo = await getNameInfo(verifiedName)
}
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress)
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress)
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
const adminAddresses = minterAdmins.map(m => m.member)
const existingAdmin = adminAddresses.includes(accountAddress)
const existingMinter = minterGroupAddresses.includes(accountAddress)
levelText = ` - Level ${addressInfo.level}</h3>`
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier, true) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
showRemoveHtml = removeActionsHtml
if (userVote === 0) {
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
} else if (userVote === 1) {
cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want
}
const confirmedKick = finalKickTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const pendingKick = pendingKickTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const confirmedBan = finalBanTxs.some(
(tx) => tx.groupId === 694 && tx.offender === accountAddress
)
const pendingBan = pendingBanTxs.some(
(tx) => tx.groupId === 694 && tx.offender === accountAddress
)
if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){
console.warn(`account was already banned, displaying as such...`)
cardColorCode = 'rgb(24, 3, 3)'
altText = `<h4 style="color:rgb(106, 2, 2); margin-bottom: 0.5em;">BANNED From MINTER Group</h4>`
showRemoveHtml = ''
if (!adminBoardState.bannedCards.has(cardIdentifier)){
adminBoardState.bannedCards.add(cardIdentifier)
}
if (!showKickedBanned){
console.warn(`kick/bank checkbox is unchecked, and card is banned, not displaying...`)
return ''
}
}
if (kickTransactions.some((kickTx) => kickTx.groupId === 694 && kickTx.member === accountAddress)){
// If user is definitely admin (finalAdd) and not pending removal
if (confirmedKick && !pendingKick && !existingMinter) {
console.warn(`account was already kicked, displaying as such...`)
cardColorCode = 'rgb(29, 7, 4)'
altText = `<h4 style="color:rgb(143, 117, 21); margin-bottom: 0.5em;">KICKED From MINTER Group</h4>`
@ -1361,6 +1336,20 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
return ''
}
}
if (confirmedBan && !pendingBan && !pendingKick && !existingMinter) {
console.warn(`account was already banned, displaying as such...`)
cardColorCode = 'rgb(24, 3, 3)'
altText = `<h4 style="color:rgb(106, 2, 2); margin-bottom: 0.5em;">BANNED From MINTER Group</h4>`
showRemoveHtml = ''
if (!adminBoardState.bannedCards.has(cardIdentifier)){
adminBoardState.bannedCards.add(cardIdentifier)
}
if (!showKickedBanned){
console.warn(`kick/bank checkbox is unchecked, and card is banned, not displaying...`)
return ''
}
}
} else {
console.log(`name could not be validated, assuming topic card (or some other issue with name validation) for removalActions`)

View File

@ -1,121 +1,320 @@
let currentMinterToolPage = 'overview'; // Track the current page
// Load latest state for admin verification
async function verifyMinterAdminState() {
const minterGroupAdmins = await fetchMinterGroupAdmins();
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin);
}
async function loadMinterAdminToolsPage() {
const loadMinterAdminToolsPage = async () => {
// Remove all body content except for menu elements
const bodyChildren = document.body.children;
for (let i = bodyChildren.length - 1; i >= 0; i--) {
const child = bodyChildren[i];
if (!child.classList.contains('menu')) {
child.remove();
child.remove()
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`
// Set the background image directly from a file
const mainContent = document.createElement('div');
const mainContent = document.createElement('div')
// In your 'AdminTools' code
mainContent.innerHTML = `
<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> <h1 style="font-size: 50px; margin: 0;">MINTER ADMIN TOOLS </h1><a style="color: red;">Under Construction...</a></div>
<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;">
<span>${userState.accountName || 'Guest'}</span>
</div>
<div><h2>COMING SOON...</h2></div>
<div>
<p>This page will have functionality to assist the Minter Admins in performing their duties. It will display all pending transactions (of any kind they can approve/deny) along with that ability. It can also be utilized to obtain more in-depth information about existing accounts.</p>
<p> The page will be getting a significant overhaul in the near(ish) future, as the MINTER group is now owned by null, and we are past the 'temporary state' we were in for much longer than planned.</p>
</div>
<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="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;">
<span>${userState.accountName || 'Guest'}'s Admin Tools</span>
</div>
<div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons">
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
</div>
<div id="tools-window" class="tools-window"></div>
<div>
<p style="color:rgba(80, 9, 9, 0.63)"></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>
`;
document.body.appendChild(mainContent);
<div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
<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" style="background-color:rgb(82, 114, 145)">Create and Display Pending Group Invites</button>
</div>
<div id="tools-window" class="tools-window" style="margin-top: 2em;">
<div id="blocklist-container" class="blocklist-form" style="display: none;">
<h3 style="margin-top: 0;">Comment Block List</h3>
<div id="blocklist-display" class="blocklist-display" style="margin-bottom: 1em;"></div>
<input
type="text"
id="blocklist-input"
class="blocklist-input"
placeholder="Enter name to block/unblock"
style="margin-bottom: 1em;"
/>
<div class="blocklist-button-container publish-card-form">
<button id="blocklist-add-button" class="publish-card-button">Add</button>
<button id="blocklist-remove-button" class="publish-card-button">Remove</button>
</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>
`
document.body.appendChild(mainContent)
addToolsPageEventListeners();
await addToolsPageEventListeners()
}
function addToolsPageEventListeners() {
document.getElementById("display-pending").addEventListener("click", async () => {
await displayPendingApprovals();
});
const addToolsPageEventListeners= async () => {
document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
const container = document.getElementById("blocklist-container")
// toggle show/hide
container.style.display = (container.style.display === "none" ? "flex" : "none")
// if showing, load the block list
if (container.style.display === "flex") {
const currentBlockList = await fetchBlockList()
displayBlockList(currentBlockList)
}
})
document.getElementById("blocklist-add-button").addEventListener("click", async () => {
const blocklistInput = document.getElementById("blocklist-input")
const nameToAdd = blocklistInput.value.trim()
if (!nameToAdd) return
// fetch existing
const currentBlockList = await fetchBlockList()
// add if not already in list
if (!currentBlockList.includes(nameToAdd)) {
currentBlockList.push(nameToAdd)
}
// publish updated
await publishBlockList(currentBlockList)
displayBlockList(currentBlockList)
blocklistInput.value = ""
alert(`"${nameToAdd}" added to the block list!`)
})
// Remove
document.getElementById("blocklist-remove-button").addEventListener("click", async () => {
const blocklistInput = document.getElementById("blocklist-input")
const nameToRemove = blocklistInput.value.trim()
if (!nameToRemove) return
// fetch existing
let currentBlockList = await fetchBlockList()
// remove if present
currentBlockList = currentBlockList.filter(name => name !== nameToRemove)
// publish updated
await publishBlockList(currentBlockList)
displayBlockList(currentBlockList)
blocklistInput.value = ""
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 () => {
createPendingGroupInvite();
});
document.getElementById("create-promotion").addEventListener("click", async () => {
createPendingPromotion();
});
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)
}
})
}
// Fetch and display pending approvals
async function displayPendingApprovals() {
console.log("Fetching pending approval transactions...");
const response = await qortalRequest({
action: "SEARCH_TRANSACTIONS",
txGroupId: 694,
txType: [
"ADD_GROUP_ADMIN",
"GROUP_INVITE"
],
confirmationStatus: "UNCONFIRMED",
limit: 0,
offset: 0,
reverse: false
});
const displayBlockList = (blockedNames) => {
const blocklistDisplay = document.getElementById("blocklist-display")
if (!blockedNames || blockedNames.length === 0) {
blocklistDisplay.innerHTML = "<p>No blocked users currently.</p>"
return
}
blocklistDisplay.innerHTML = `
<ul>
${blockedNames.map(name => `<li>${name}</li>`).join("")}
</ul>
`
}
console.log("Fetched pending approvals: ", response);
const toolsWindow = document.getElementById('tools-window');
if (response && response.length > 0) {
toolsWindow.innerHTML = response.map(tx => `
<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
<p><strong>Transaction Type:</strong> ${tx.type}</p>
<p><strong>Amount:</strong> ${tx.amount}</p>
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
<p><strong>Recipient:</strong> ${tx.recipient}</p>
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
</div>
`).join('');
} else {
toolsWindow.innerHTML = '<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;"><p>No pending approvals found.</p></div>';
const fetchPendingInvites = async () => {
try {
const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions()
return pendingInviteTxs
} catch (err) {
console.error("Error fetching pending invites:", err)
return []
}
}
// Placeholder function to create a pending group invite
async function createPendingGroupInvite() {
console.log("Creating a pending group invite...");
// Placeholder code for creating a pending group invite
alert('Pending group invite created (placeholder).');
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.`)
}
// Placeholder function to create a pending promotion
async function createPendingPromotion() {
console.log("Creating a pending promotion...");
// Placeholder code for creating a pending promotion
alert('Pending promotion created (placeholder).');
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
}
// Placeholder function for approving a transaction
function approveTransaction(signature) {
console.log("Approving transaction with signature: ", signature);
// Placeholder code for approving transaction
alert(`Transaction with signature ${signature} approved (placeholder).`);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
const Q_MINTERSHIP_VERSION = "1.22"
const messageIdentifierPrefix = `mintership-forum-message`
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
@ -66,6 +68,10 @@ if (localStorage.getItem("latestMessageIdentifiers")) {
document.addEventListener("DOMContentLoaded", async () => {
console.log("DOMContentLoaded fired!")
createScrollToTopButton()
document.querySelectorAll(".version").forEach(el => {
el.textContent = `Q-Mintership (v${Q_MINTERSHIP_VERSION}b)`
})
// --- GENERAL LINKS (MINTERSHIP-FORUM and MINTER-BOARD) ---
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]')
@ -94,7 +100,6 @@ document.addEventListener("DOMContentLoaded", async () => {
await loadScript("./assets/js/MinterBoard.js")
}
await loadMinterBoardPage()
createScrollToTopButton()
})
})

View File

@ -6,6 +6,11 @@ let baseUrl = ''
let isOutsideOfUiDevelopment = false
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') {
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
isOutsideOfUiDevelopment = false
@ -53,7 +58,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}.${month}.${day}@${hours}:${minutes}:${seconds}`
const formattedDate = `${day}.${month}.${year}..@${hours}:${minutes}:${seconds}`
console.log('Formatted date:', formattedDate)
return formattedDate
}
@ -146,7 +151,7 @@ const base64ToUint8Array = async (base64) => {
}
return bytes
}
}
const uint8ArrayToObject = async (uint8Array) => {
// Decode the byte array using TextDecoder
@ -157,7 +162,7 @@ const uint8ArrayToObject = async (uint8Array) => {
const obj = JSON.parse(jsonString)
return obj
}
}
const objectToBase64 = async (obj) => {
@ -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 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 {
const response = await fetch (`${baseUrl}/addresses/${address}`, {
headers: { 'Accept': 'application/json' },
@ -248,11 +266,24 @@ 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) => {
console.log('fetchOwnerAddressFromName called')
console.log('name:', name)
try {
const response = await fetch(`${baseUrl}/names/${name}`, {
const response = await fetch(`${baseUrl}/names/${encodeURIComponent(name)}`, {
headers: { 'Accept': 'application/json' },
method: 'GET',
})
@ -326,12 +357,21 @@ const verifyAddressIsAdmin = async (address) => {
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) => {
console.log('getNameInfo called')
console.log('name:', name)
try {
const response = await fetch(`${baseUrl}/names/${name}`)
const response = await fetch(`${baseUrl}/names/${encodeURIComponent(name)}`)
if (!response.ok) {
console.warn(`Failed to fetch name info for: ${name}, status: ${response.status}`)
@ -438,17 +478,17 @@ const login = async () => {
}
const getNameFromAddress = async (address) => {
try {
const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
})
const names = await response.json()
return names.length > 0 ? names[0].name : address // Return name if found, else return address
} catch (error) {
console.error(`Error fetching names for address ${address}:`, error)
return address
}
try {
const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
})
const names = await response.json()
return names.length > 0 ? names[0].name : address // Return name if found, else return address
} catch (error) {
console.error(`Error fetching names for address ${address}:`, error)
return address
}
}
@ -491,28 +531,6 @@ const fetchMinterGroupAdmins = async () => {
//use what is returned .member to obtain each member... {"member": "memberAddress", "isAdmin": "true"}
}
// const fetchAllAdminGroupsMembers = async () => {
// try {
// let adminGroupMemberAddresses = [] // Declare outside loop to accumulate results
// for (const groupID of adminGroupIDs) {
// const response = await fetch(`${baseUrl}/groups/members/${groupID}?limit=0`, {
// method: 'GET',
// headers: { 'Accept': 'application/json' },
// })
// const groupData = await response.json()
// if (groupData.members && Array.isArray(groupData.members)) {
// adminGroupMemberAddresses.push(...groupData.members) // Merge members into the array
// } else {
// console.warn(`Group ${groupID} did not return valid members.`)
// }
// }
// return adminGroupMemberAddresses
// } catch (error) {
// console.log('Error fetching admin group members', error)
// }
// }
const fetchAllAdminGroupsMembers = async () => {
try {
// We'll track addresses so we don't duplicate the same .member
@ -545,7 +563,7 @@ const fetchAllAdminGroupsMembers = async () => {
console.error('Error fetching admin group members', error)
return []
}
}
}
const fetchMinterGroupMembers = async () => {
try {
@ -651,7 +669,7 @@ const fetchGroupInvitesByAddress = async (address) => {
console.error('Error fetching address group invites:', error)
throw error
}
}
}
// QDN data calls --------------------------------------------------------------------------------------------------
const searchLatestDataByIdentifier = async (identifier) => {
@ -804,22 +822,24 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => {
// NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object.
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true, after=0) => {
try {
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&fter=${after}`
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&after=${after}`
if (name && !identifier && !room) {
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) {
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) {
console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`)
return null
} 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}`, {
@ -1141,7 +1161,7 @@ const base64ToBlob = (base64String, mimeType) => {
}
// Create a blob from the Uint8Array
return new Blob([bytes], { type: mimeType })
}
}
const base64ToBlobUrl = (base64, mimeType) => {
const binary = atob(base64)
@ -1193,7 +1213,7 @@ const base64ToBlobUrl = (base64, mimeType) => {
console.error("Skipping file due to error in fetchEncryptedImageBase64:", error)
return null // indicates "missing or failed"
}
}
}
@ -1253,6 +1273,20 @@ const getProductDetails = async (service, name, identifier) => {
// 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) => {
try {
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
@ -1281,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) => {
try {
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
@ -1363,12 +1406,12 @@ const processTransaction = async (signedTransaction) => {
console.error("Error processing transaction:", error)
throw error
}
}
}
// 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.
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive, txGroupId, fee) => {
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive=0, txGroupId, fee) => {
try {
// Fetch account reference correctly
@ -1417,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 {
// Fetch account reference correctly
const accountInfo = await getAddressInfo(recipientAddress)
const accountInfo = await getAddressInfo(member)
const accountReference = accountInfo.reference
// Validate inputs before making the request
if (!adminPublicKey || !accountReference || !recipientAddress) {
throw new Error("Missing required parameters for group invite transaction.")
if (!adminPublicKey || !accountReference || !member) {
throw new Error("Missing required parameters for group kick transaction.")
}
const payload = {
@ -1434,11 +1477,10 @@ const createGroupKickTransaction = async (recipientAddress, adminPublicKey, grou
reference: accountReference,
fee,
txGroupId,
recipient: null,
adminPublicKey,
groupId: groupId,
member: member || recipientAddress,
reason: reason
groupId,
member,
reason
}
console.log("Sending GROUP_KICK transaction payload:", payload)
@ -1860,15 +1902,16 @@ const searchTransactions = async ({
console.error("Error in searchTransactions:", error)
throw error
}
}
}
const searchPendingTransactions = async (limit = 20, offset = 0) => {
const searchPendingTransactions = async (limit=20, offset=0, reverse=false) => {
try {
const queryParams = []
if (limit) queryParams.push(`limit=${limit}`)
if (offset) queryParams.push(`offset=${offset}`)
if (reverse) queryParams.push(`reverse=${reverse}`)
const queryString = queryParams.join('&');
const queryString = queryParams.join('&')
const url = `${baseUrl}/transactions/pending${queryString ? `?${queryString}` : ''}`
const response = await fetch(url, {
@ -1877,11 +1920,11 @@ const searchPendingTransactions = async (limit = 20, offset = 0) => {
})
if (!response.ok) {
const errorText = await response.text();
const errorText = await response.text()
throw new Error(`Failed to search pending transactions: HTTP ${response.status}, ${errorText}`)
}
const result = await response.json();
const result = await response.json()
if (!Array.isArray(result)) {
throw new Error("Expected an array for pending transactions, but got something else.")
}
@ -1891,5 +1934,5 @@ const searchPendingTransactions = async (limit = 20, offset = 0) => {
console.error("Error in searchPendingTransactions:", error)
throw error
}
}
}

220
assets/js/Shared.js Normal file
View File

@ -0,0 +1,220 @@
// This is a Helper Script that will contain the functions that are accessed from multiple different scripts in the app. Allowing this script to be loaded first, will ensure they all have awareness of them and will allow future development to be simpler.
let blockedNamesIdentifier = 'Q-Mintership-blockedNames'
const fetchBlockList = async () => {
try {
// searchSimple to find all resources for that identifier
const results = await searchSimple(
'BLOG_POST',
blockedNamesIdentifier, // identifier
'', // name
0, // limit=0 => no limit
0, // offset
'', // room
true, // reverse => newest first or oldest first?
true // prefixOnly => depends on whether you want partial matches
)
if (!results || !Array.isArray(results) || results.length === 0) {
console.warn("No blockList resources found via searchSimple.")
return []
}
// We must filter out resources not published by an admin
const adminGroupMembers = await fetchAllAdminGroupsMembers()
const adminAddresses = adminGroupMembers.map(m => m.member)
// The result objects from searchSimple have shape: { name, identifier, service, created, updated, ... }
// We want only those where 'name' is an admin address's name, or the 'address' is in adminAddresses
// But searchSimple doesn't give you the publisher address directly, only the name.
// So we must check if the name belongs to an admin address
const validAdminResults = []
for (const r of results) {
try {
// fetchOwnerAddressFromName or getNameInfo to see if r.name resolves to one of the admin addresses
const nameInfo = await getNameInfo(r.name)
if (!nameInfo || !nameInfo.owner) {
continue
}
if (adminAddresses.includes(nameInfo.owner)) {
validAdminResults.push(r)
}
} catch (err) {
console.warn(`Skipping result from ${r.name} - cannot confirm admin address`, err)
}
}
if (validAdminResults.length === 0) {
console.warn("No valid admin-published blockList resource found.")
return []
}
// pick the newest result among validAdminResults
// Usually you check r.updated or r.created
validAdminResults.sort((a, b) => {
const tA = a.updated || a.created || 0
const tB = b.updated || b.created || 0
return tB - tA // newest first
})
const newestValid = validAdminResults[0]
// fetch the actual data
const resourceData = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: newestValid.name,
service: newestValid.service, // "BLOG_POST"
identifier: newestValid.identifier
})
if (!resourceData) {
console.warn("Fetched resource data is null/empty.")
return []
}
// parse resourceData
// If it's a string containing base64 JSON
let blockedList
if (typeof resourceData === 'string') {
// decode base64 => parse JSON
const decoded = atob(resourceData)
blockedList = JSON.parse(decoded)
} else if (Array.isArray(resourceData)) {
// the resource is already an array
blockedList = resourceData
} else {
// maybe resourceData has data64 property or something else
// adapt if needed
console.warn("Unexpected blockList format. Could not parse.")
return []
}
if (!Array.isArray(blockedList)) {
console.warn("Block list is not an array:", blockedList)
return []
}
console.log("Newest block list loaded:", blockedList)
return blockedList
} catch (err) {
console.error("Failed to load block list:", err)
return []
}
}
const publishBlockList = async (blockedNames) => {
if (!Array.isArray(blockedNames)) {
console.warn("publishBlockList requires an array")
return
}
try {
const jsonStr = JSON.stringify(blockedNames)
const data64 = btoa(jsonStr)
// Publish
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: `${userState.accountName}`, // The name under which your admin can publish
service: "BLOG_POST",
identifier: `${blockedNamesIdentifier}`,
data64
})
alert("Block list published successfully!")
} catch (err) {
console.error("Failed to publish block list:", err)
alert("Error publishing block list.")
}
}
// Function for obtaining all kick/ban transaction data, and separating it into PENDING and NON.
const fetchAllKickBanTxData = async () => {
const kickTxType = "GROUP_KICK"
const banTxType = "GROUP_BAN"
const allKickTx = await searchTransactions({
txTypes: [kickTxType],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
const allBanTx = await searchTransactions({
txTypes: [banTxType],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
const { finalTx: finalKickTxs, pendingTx: pendingKickTxs } = partitionTransactions(allKickTx)
const { finalTx: finalBanTxs, pendingTx: pendingBanTxs } = partitionTransactions(allBanTx)
// We are going to keep all transactions in order to filter more accurately for display purposes.
console.log('Final kickTxs:', finalKickTxs);
console.log('Pending kickTxs:', pendingKickTxs);
console.log('Final banTxs:', finalBanTxs);
console.log('Pending banTxs:', pendingBanTxs);
return {
finalKickTxs,
pendingKickTxs,
finalBanTxs,
pendingBanTxs,
}
}
const partitionTransactions = (txSearchResults) => {
const finalTx = []
const pendingTx = []
for (const tx of txSearchResults) {
if (tx.approvalStatus === 'PENDING') {
pendingTx.push(tx)
} else {
finalTx.push(tx)
}
}
return { finalTx, pendingTx };
}
const fetchAllInviteTransactions = async () => {
const inviteTxType = "GROUP_INVITE"
const allInviteTx = await searchTransactions({
txTypes: [inviteTxType],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx)
console.log('Final InviteTxs:', finalInviteTxs)
console.log('Pending InviteTxs:', pendingInviteTxs)
return {
finalInviteTxs,
pendingInviteTxs,
}
}
const findPendingApprovalsForTxSignature = async (txSignature, txType='GROUP_APPROVAL', limit=0, offset=0) => {
const pendingTxs = await searchPendingTransactions(limit, offset)
// Filter only the relevant GROUP_APPROVAL TX referencing txSignature
const approvals = pendingTxs.filter(tx =>
tx.type === txType && tx.pendingSignature === txSignature
)
console.log(`approvals found:`,approvals)
return approvals
}

View File

@ -28,8 +28,14 @@
<link rel="preload" href="./assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript>
<link href="./assets/quill/quill.snow.css" rel="stylesheet">
</head>
<body>
<script>
// Variable-based versioning (credit: QuickMythril)
; // Update here in the future
</script>
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
@ -42,7 +48,7 @@
</a>
</span>
<span class="navbar-caption-wrap">
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.04b)
<a class="navbar-caption display-4" href="index.html"><span class="navbar-caption display-4 version"></span>
</a>
</span>
</div>
@ -61,12 +67,12 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a>
</span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.04b<br></a></span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html"><span class="navbar-caption display-4 version"></span><br></a></span>
</div>
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">MAM Board</a></div>
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">MAM BOARD</a></div>
</div>
</div>
@ -84,7 +90,7 @@
<img src="assets/images/mbr-1623x1082.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
<div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">
Admin Board</h2>
ADMIN BOARD</h2>
</div>
</div>
</a>
@ -93,7 +99,7 @@
<div class="item-wrapper">
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
<div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">MinterBoard</h2>
<h2 class="card-title mbr-fonts-style display-2">MINTER BOARD</h2>
</div>
</div>
</a>
@ -109,7 +115,7 @@
<img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
<div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">
Minter Admin Management (MAM) Board</h2>
MAM BOARD</h2>
</div>
</div>
</a>
@ -118,7 +124,7 @@
<div class="item-wrapper">
<img src="assets/images/mbr-1-1818x1212.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
<div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">Mintership Forum</h2>
<h2 class="card-title mbr-fonts-style display-2">Q-MINTERSHIP FORUM</h2>
</div>
</div>
</a>
@ -135,7 +141,7 @@
<div class="row">
<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">
<span class="mbr-iconfont mbr-iconfont-btn mbri-file" style="color:aliceblue;"></span>
</div>
@ -188,378 +194,26 @@
</div>
</section>
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v1.04beta 01-27-2025</h2>
<h2 class="mbr-section-title mbr-fonts-style display-2 version">
</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.04b Fixes</u></b>- <b>MANY fixes </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for details, too many to list here.
<!-- <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>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v1.03beta 01-23-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.03b Fixes</u></b>- <b>Filtering issue resolved </b> - Version 1.02 had a filtering logic modification applied to add and remove admin transactions. However, it was not changed on the REMOVE filtering, so REMOVE transactions that were PENDING were showing as COMPLETE and thus the board was displaying cards as already removed when there was only a PENDING tx. This has been resolved in 1.03b.
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v1.02beta 01-22-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.02b Fixes</u></b>- <b>There were publish issues on ARA Board</b> - These publish issues have now been resolved. DUPLICATES ARE NOT ALLOWED, and if you did not publish a card for a name, you cannot publish an update. Also removed the 'alert' for comment publish, as it was unnecessary. Also resolved an issue preventing a user from publishing more than a single card (regardless of duplicates).
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v1.01beta 01-21-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.01b - Improving Major changes in 1.0.</u></b>- <b>Every feature required for the new featureTriggers have been added</b>, and a minor bug on the ARA Board has been resolved. Pull Request from QuickMythril has also been added. Allowing cards to be arranged based on various parameters.
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.84beta 01-13-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>NEW Features</u></b>- <b>All GROUP_APPROVAL and Transaction functionality</b> - Ability to CREATE INVITE, KICK, AND BAN transactions, and functionality to allow GROUP_APPROVAL of said transactions. Checks for the featureTrigger and whether it has passed or not, to change the functionality to the new methods post-core-update. See page 15 of the General Room for details - <a href="MINTERSHIP-FORUM">FORUM</a> </p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b><u>JOIN MINTER GROUP</u></b> - <b>Join the Minter Group from your Minter Card</b> - After the minter who published a card is APPROVED (with GROUP_APPROVAL after next core update, or by an invite from crowetic prior, after 40%+ approval by Minter Admins...) a 'JOIN MINTER GROUP' button will appear on the card for the user that published it. Upon clicking this button a JOIN_GROUP transaction will be created, and processed, thus accepting the invite to the MINTER group. </p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b><u>CHECK THE ANNOUNCEMENTS</b></u> - <b>in the <a href="MINTERSHIP-FORUM">FORUM</a> </b> </p><p>on the 15th page of General room, and other related information in the MINTER ROOM on page 5.</p>
<p class="mbr-text mbr-fonts-style display-7"><b>Various additional fixes and cleanup</b>. All of the above functionality has been TESTED on the DevNet, that is why there is a big jump in VERSION from the last to this one.</p><p>Many additional features are coming soon, including a NOTIFICATION SYSTEM, PROFILES AND EXPLORER, and MUCH MORE. Q-Mintership will become more than simply 'the app used to become a minter'. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.71beta 01-08-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>NEW Features</u></b>- <b>'KICK, BAN, and GROUP_APPROVALS'</b> - Features to ADD and REMOVE minters from MINTER group, and GROUP_APPROVAL after featureTrigger block passes have all been added. <a href="MINTERSHIP-FORUM">FORUM</a> </p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b><u>NEW Feature</u></b> - <b>'Colorized Card Statuses'</b> - Colorized Card statuses based on invite status. Cards will become RED/ORANGE in AdminBoard if they have been KICKED/BANNED. Cards will become BLACK in the MinterBoard if they have been INVITED. </p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b><u>NEW Feature</b></u> - <b>JOIN BUTTON</b> - MinterBoard will have a JOIN button come up on the card for the minter that has been INVITED if that minter views the board.</p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b>Various additional fixes and cleanup</b>. Ability to hide cards, search functionality, and additional profile and account data will be coming soon.</p><p></p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.71beta 01-04-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>NEW Feature</u></b>- <b>'INVITE MINTER'</b> - This is a button that will come up on the Minter Board and allow existing Minters (non-admins) to create the INVITE transaction that will then be approved by the Minter Admins. The concept of the 'Minter Admin Tools' section is changing, and the tools for creation and approval of transactions are being implemented into the Minter Board instead. Just as the Votes are now displayed, in the future the approval transactions will be displayed. Making it a one-stop location for all new (and existing) Minter details and actions. More details will be published in the <a href="MINTERSHIP-FORUM">FORUM</a> </p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b><u>NEW Feature</u></b> - <b>'ScrollToTop button'</b> - The 'ScrollToTop' button was a requested feature to allow users to easily scroll back to the top of the page. It will come up on any page after you scroll down over 100px, and allow you to get back to the top of the page with a single click. Applied to Forum and Boards.</p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b><u>Fixes</b></u> - <b>Admin Room image embeds</b> - The image embed feature on the Forum, in the Admin Room (encrypted) has been fixed, attached images will now display in the preview pane as they would with unencrypted images.</p><p></p>
<p class="mbr-text mbr-fonts-style display-7"><b>Various additional fixes and cleanup</b>. More account detail features and the modification of the Minter Admin Tools section into an 'account details explorer' will be taking place over time.</p><p></p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.70beta 01-03-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
A few update patches have been made, so this is a patch update to fix various issues. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.68beta 01-01-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
This is a patch update to fix loading of data into minter cards upon duplicates, and clearing message input on send in the forum.</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.67beta 12-31-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
Fixes for name-based cards on the admin board mostly.</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v0.66beta 12-30-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
New fixes for fake names, and not displaying minters that are already minters. Also, fix for QuickMythril 'poll hijack'. Fixed displaying of encrypted images in Minter Room, and more code cleanup.</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
New Version + Features 12-28-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
New Version now includes full POLL RESULT DETAILS for every card. This includes each vote, the voter name, their weight (blocksMinted), and much more. Many additional code-cleanup changes were made as well. Also... The ARBITRARY REBUILD BOOTSTRAP should now be available, IF YOU ARE HAVING ISSUES SEEING DATA, PLEASE BOOTSTRAP YOUR NODE TO OBTAIN A REBUILT DATABASE. Thank you!</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
publish fix+ Notes 12-28-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
Fixed an issue causing publishes to fail, after cleaning up part of the code here. Also, a re-built db with the archive rebuild run on it, that should help significantly with 'missing data' on nodes, is now on the bootstrap cluster. Once a new bootstrap has been created an announcement will be made. Thank you!</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Massive performance + Notes 12-27-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
MASSIVE performance improvements today. Increased performance of both the Forum and Minter Board by a HUGE margin today by leveraging async better, and utilizing searchSimple for all search calls. This makes the forum and boards load 100x faster. Also, may have determined the cause of the QDN bug... we will research further, but as of now, I am definitely getting better results withOUT following names. Will update as time goes on. Also... PLEASE NOTE - POLLS ARE NOT TO BE UTILIZED THROUGH QOMBO OR ANY OTHER APP. Polls in the Minter and Admin Boards are TIED to the cards that published them, and the results are FILTERED to display only the results of MINTERS and ADMINS. Therefore, utilizing outside tools to read (or create) polls is not only an exercise in futility, but also will provide no useful information whatsoever. Please realize that the poll data is utilized to show direct support of the cards by minters and admins, and pols are NOT MEANT TO BE CREATED OR VIEWED OUTIDE OF Q-MINTERSHIP. Thank you. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Issues from QDN 12-27-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
it seems there were some issues caused by what can only be described as the QDN bug. I will be checking into this in more detail in the near future, but it seems like the previous publish did not get the update as it was supposed to out to many people, and as such many of the changes were not there in the code that was on each node. Due to this, many issues were happening. Including the fact that the identifier change didn't take place, or at least that is what seems to have happened. Will verify this shortly. Until then will publish another update now that should resolve some lingering issues. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Yet More Fixes + Updates 12-26-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
Another update has been accomplished, this time to version 0.61beta. This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Huge changes and fixes 12-23-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
All of the Board and Forum bugs that were known have been resolved. There are still future changes to be made in order to add searching, etc. ALL IDENTIFIERS ARE SET TO PERMANENT IDENTIFIERS NOW. This means that all previous publishes that were done in testMode will no longer show up, and the app is now in 'officially released' status. Announcements about the MinterBoard can now be made publicly. Admin Board now allows publishing non-name-based cards with 'topics' instead, for use in admin voting. Encrypted images and attachments now work and download/display as they do in the unencrypted rooms on the forum. Much more to come, and much more has been done. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Big Updates to Boards 12-20-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
The Minter and Admin boards received large updates today. Many modificatons. It should be about time to do public announcements about the Minter Board. New types of boards are in planning as well. The changes to the Mintership Forum Admins Room to allow encrypted file downloads are taking a bit longer than expected, but should be completed soon as well. Hopefully if all goes well the public announcements will take place on Monday.</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
a few things remaining</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
There are still a few things remaining, such as downloading of encrypted attachments in the Admin Room on the forum, and final testing of the Minter Data Board, with potentially a rename of that section to 'Minter Management' in the future. Many additional changes will be coming as time goes on as well. Turning Q-Mintership-Alpha into a fully featured Mintership app! Hope you enjoy it and that it is useful for you, and if you have any suggestions in regard to new features or modifications to existing ones, <a href="qortal://APP/Q-Mail/to/crowetic">send a Q-Mail message to crowetic.</a></p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Updates 12.17.2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
The Q-Mintership-Alpha application is now published on the Q-Mintership name, and has a plethora of new features. Including the Minter Board, where new minters will publish information about themselves so that the admins can make informed decisions, and the Minter Data Board as a new Minter Admin Tool to manage the data regarding minters they want to add/remove from the MINTER group. The Forum portion of the app now also contains an encrypted Admin room, and has had a ton of improvements made as well. </p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
This is the beginning...</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
This is the very start of the Q-Mintership app. It will be dramatically changing upon the beta release, and modification to the Q-Mintership Q-App. This initial version is a version that could be launched more quickly, and does not have nearly as much functionality as what will exist once the main app goes live.&nbsp;</p>
</div>
</div>
</div>
</div>
</section>
@ -572,12 +226,12 @@
<div class="title-wrapper">
<div class="title-wrap">
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.04b)</h2>
<h2 class="mbr-section-title mbr-fonts-style display-5"><span class="navbar-caption display-4 version"></span></h2>
</div>
</div>
<a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.04beta</p>
<p class="mbr-link mbr-fonts-style display-4"><span class="navbar-caption display-4 version"></span></p>
</a>
</div>
<div class="col-12 col-lg-6">
@ -590,7 +244,6 @@
</div>
</section>
<script src="./assets/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="./assets/parallax/jarallax.js"></script>
<!-- <script src="./assets/smoothscroll/smooth-scroll.js"></script> -->
@ -598,12 +251,13 @@
<script src="./assets/dropdown/js/navbar-dropdown.js"></script>
<script src="./assets/theme/js/script.js"></script>
<script src="./assets/quill/quill.min.js"></script>
<!-- Order here MATTERS, we MUST load the scripts in the correct order so that all the functions will work properly -->
<script src="./assets/js/QortalApi.js"></script>
<script src="./assets/js/Shared.js"></script>
<script src="./assets/js/MinterBoard.js"></script>
<script src="./assets/js/AdminTools.js"></script>
<script src="./assets/js/AdminBoard.js"></script>
<script src="./assets/js/ARBoard.js"></script>
<script src="./assets/js/AdminTools.js"></script>
<script src="./assets/js/Q-Mintership.js"></script>
<input name="animation" type="hidden">
</body>