diff --git a/assets/css/forum-styles.css b/assets/css/forum-styles.css index bbd7c50..0be7fbc 100644 --- a/assets/css/forum-styles.css +++ b/assets/css/forum-styles.css @@ -584,7 +584,7 @@ body { border-radius: 5px; padding: 20px; margin: 20px auto; /* center horizontally */ - max-width: 600px; /* limit width */ + /* max-width: 600px; */ color: #ddd; /* text color */ text-align: center; align-items: center; @@ -596,7 +596,7 @@ body { background-color:#000000; width: 90%; font-size: 1.8rem; - color: #4d0000; + color: #fff3f3; text-align: center; align-items: center; /* you could style the list items or bullet if you like */ @@ -616,7 +616,17 @@ body { background-color: #14161a; border: 1px solid #8caeb0; border-radius: 4px; - color: #5c0101; + color: #f19c9c; +} + +.invite-form input.invite-input { + padding: 1rem; + font-size: 2rem; + line-height: 2; + background-color: #14161a; + border: 1px solid #8caeb0; + border-radius: 4px; + color: #dddddd; } .publish-card-button { @@ -707,6 +717,43 @@ body { 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) { diff --git a/assets/js/AdminTools.js b/assets/js/AdminTools.js index f1c296d..b2d62df 100644 --- a/assets/js/AdminTools.js +++ b/assets/js/AdminTools.js @@ -33,7 +33,7 @@ const loadMinterAdminToolsPage = async () => {
- +
@@ -55,6 +55,30 @@ const loadMinterAdminToolsPage = async () => {
+ + + @@ -63,10 +87,10 @@ const loadMinterAdminToolsPage = async () => { document.body.appendChild(mainContent) - addToolsPageEventListeners() + await addToolsPageEventListeners() } -function addToolsPageEventListeners() { +const addToolsPageEventListeners= async () => { document.getElementById("toggle-blocklist-button").addEventListener("click", async () => { const container = document.getElementById("blocklist-container") // toggle show/hide @@ -116,6 +140,32 @@ function addToolsPageEventListeners() { alert(`"${nameToRemove}" removed from the block list (if it was present).`) }) + document.getElementById("invite-user-button").addEventListener("click", async () => { + const inviteInput = document.getElementById("invite-input") + const nameOrAddress = inviteInput.value.trim() + if (!nameOrAddress) return + + try { + // We'll call some function handleManualInvite(nameOrAddress) + await handleManualInvite(nameOrAddress) + inviteInput.value = "" + + } catch (err) { + console.error("Error inviting user:", err) + alert("Failed to invite user.") + } + }) + + document.getElementById("create-group-invite").addEventListener("click", async () => { + const inviteContainer = document.getElementById("invite-container") + // Toggle display + inviteContainer.style.display = (inviteContainer.style.display === "none" ? "flex" : "none") + // If showing, load the pending invites + if (inviteContainer.style.display === "flex") { + const pendingInvites = await fetchPendingInvites() + await displayPendingInviteDetails(pendingInvites) + } + }) } const displayBlockList = (blockedNames) => { @@ -131,4 +181,139 @@ const displayBlockList = (blockedNames) => { ` } +const fetchPendingInvites = async () => { + try { + const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions() + return pendingInviteTxs + } catch (err) { + console.error("Error fetching pending invites:", err) + return [] + } +} + +const handleManualInvite = async (nameOrAddress) => { + const addressInfo = await getAddressInfo(nameOrAddress) + let address = addressInfo.address + if (addressInfo && address) { + console.log(`address is ${address}`) + } else { + // it might be a Qortal name => getNameInfo + const nameData = await getNameInfo(nameOrAddress) + if (!nameData || !nameData.owner) { + throw new Error(`Cannot find valid address for ${nameOrAddress}`) + } + address = nameData.owner + } + + const adminPublicKey = await getPublicKeyByName(userState.accountName) + const timeToLive = 864000 // e.g. 10 days in seconds + const fee = 0.01 + let txGroupId = 694 + + // build the raw invite transaction + const rawInviteTransaction = await createGroupInviteTransaction( + address, + adminPublicKey, + 694, + address, + timeToLive, + txGroupId, + fee + ) + + // sign + const signedTransaction = await qortalRequest({ + action: "SIGN_TRANSACTION", + unsignedBytes: rawInviteTransaction + }) + if (!signedTransaction) { + throw new Error("SIGN_TRANSACTION returned null. Possibly user canceled or an older UI?") + } + + // process + const processResponse = await processTransaction(signedTransaction) + if (!processResponse) { + throw new Error("Failed to process transaction. Possibly canceled or error from Qortal Core.") + } + + alert(`Invite transaction submitted for ${nameOrAddress}. Wait for confirmation.`) +} + + +const displayPendingInviteDetails = async (pendingInvites) => { + const invitesContainer = document.getElementById('pending-invites-display') + if (!pendingInvites || pendingInvites.length === 0) { + invitesContainer.innerHTML = "

No pending invites found.

" + return + } + + let html = `

Current Pending Invites:

` + + 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 } = await buildApprovalTableHtml(approvals, getNameFromAddress) + const finalTable = approvals.length > 0 ? tableHtml : "

No Approvals Found

" + + html += ` +
+
+ Invite Tx:

${inviteTx.signature.slice(0, 8)}...

+ Invitee:

${inviteeName}

+ Date:

${dateStr}

+ CreatorName:

${creatorName}

+ Total Approvals:

${approvalCount}

+ +
+ +
+ Existing Approvals: + ${finalTable} +
+ +
+ ` + } + + html += "
" + invitesContainer.innerHTML = html +} + diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js index d1fef92..f708279 100644 --- a/assets/js/MinterBoard.js +++ b/assets/js/MinterBoard.js @@ -1652,10 +1652,8 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => } } -const findPendingApprovalTxForAddress = async (address, txType, limit = 0, offset = 0) => { - // 1) Fetch all pending transactions +const findPendingTxForAddress = async (address, txType, limit = 0, offset = 0) => { const pendingTxs = await searchPendingTransactions(limit, offset, false) - // if a txType is passed, return the results related to that type, if not, then return any pending tx of the potential types. let relevantTypes if (txType) { relevantTypes = new Set([txType]) @@ -1710,15 +1708,15 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa blockLimit: 0, txGroupId: 0 }) - const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType, 0, 0) + const pendingTxs = await findPendingTxForAddress(address, transactionType, 0, 0) let isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin // If no pending transaction found, return null - if (!pendingApprovals || pendingApprovals.length === 0) { - console.warn("no pending approval transactions found, returning null...") + if (!pendingTxs || pendingTxs.length === 0) { + console.warn("no pending transactions found, returning null...") return null } - const txSig = pendingApprovals[0].signature - // Find the relevant signature. (First approval) + const txSig = pendingTxs[0].signature + // Find the relevant signature. (signature of the issued transaction pending.) const relevantApprovals = approvalSearchResults.filter( (approvalTx) => approvalTx.pendingSignature === txSig ) diff --git a/assets/js/Q-Mintership.js b/assets/js/Q-Mintership.js index 284bd47..9e0fb29 100644 --- a/assets/js/Q-Mintership.js +++ b/assets/js/Q-Mintership.js @@ -1,4 +1,4 @@ -const Q_MINTERSHIP_VERSION = "1.06.4" +const Q_MINTERSHIP_VERSION = "1.06.5" const messageIdentifierPrefix = `mintership-forum-message` const messageAttachmentIdentifierPrefix = `mintership-forum-attachment` diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js index d76917a..438e070 100644 --- a/assets/js/QortalApi.js +++ b/assets/js/QortalApi.js @@ -1354,7 +1354,7 @@ const processTransaction = async (signedTransaction) => { // Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days. // 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 diff --git a/assets/js/Shared.js b/assets/js/Shared.js index 61480fe..1c61602 100644 --- a/assets/js/Shared.js +++ b/assets/js/Shared.js @@ -196,13 +196,25 @@ const fetchAllInviteTransactions = async () => { const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx) - console.log('Final kickTxs:', finalInviteTxs) - console.log('Pending kickTxs:', pendingInviteTxs) + 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 +} + \ No newline at end of file