diff --git a/assets/css/forum-styles.css b/assets/css/forum-styles.css index cd98b38..556064b 100644 --- a/assets/css/forum-styles.css +++ b/assets/css/forum-styles.css @@ -437,6 +437,14 @@ cursor: pointer; } +#scrollToTopButton:hover { + background-color: white; + color: black; + border: 2px solid black; + box-shadow: 0 0 15px rgba(255,255,255,0.8); + transform: scale(1.1); /* Slight enlarge effect on hover */ +} + /* this is the text from the quill editor, hopefully these settings will prevent the page styles from affecting the formatted html from editor posts. */ .message-text { diff --git a/assets/js/AdminBoard.js b/assets/js/AdminBoard.js index 03c2d26..8e50bf4 100644 --- a/assets/js/AdminBoard.js +++ b/assets/js/AdminBoard.js @@ -99,11 +99,10 @@ const loadAdminBoardPage = async () => { document.getElementById("publish-card-form").addEventListener("submit", async (event) => { event.preventDefault() const isTopicChecked = document.getElementById("topic-checkbox").checked - // Pass that boolean to publishEncryptedCard await publishEncryptedCard(isTopicChecked) }) - + createScrollToTopButton() // await fetchAndValidateAllAdminCards() await fetchAllEncryptedCards() await updateOrSaveAdminGroupsDataLocally() diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js index 91224c7..5b036fc 100644 --- a/assets/js/MinterBoard.js +++ b/assets/js/MinterBoard.js @@ -4,6 +4,9 @@ const cardIdentifierPrefix = "Minter-board-card" let isExistingCard = false let existingCardData = {} let existingCardIdentifier = {} +const MIN_ADMIN_YES_VOTES = 9; +const MINTER_INVITE_BLOCK_HEIGHT = 9999999; // Example height, update later +let isApproved = false const loadMinterBoardPage = async () => { // Clear existing content on the page @@ -28,7 +31,7 @@ const loadMinterBoardPage = async () => {
` document.body.appendChild(mainContent) + createScrollToTopButton() document.getElementById("publish-card-button").addEventListener("click", async () => { try { @@ -331,7 +335,7 @@ const fetchExistingCard = async () => { return null } else if (response.length === 1) { // we don't need to go through all of the rest of the checks and filtering nonsense if there's only a single result, just return it. const mostRecentCard = response[0] - + isExistingCard = true const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: userState.accountName, // User's account name @@ -341,6 +345,7 @@ const fetchExistingCard = async () => { existingCardIdentifier = mostRecentCard.identifier existingCardData = cardDataResponse + isExistingCard = true return cardDataResponse } @@ -367,6 +372,7 @@ const fetchExistingCard = async () => { existingCardIdentifier = mostRecentCard.identifier existingCardData = cardDataResponse + isExistingCard = true console.log("Full card data fetched successfully:", cardDataResponse) @@ -472,6 +478,7 @@ const publishCard = async () => { if (isExistingCard){ alert("Card Updated Successfully! (No poll updates are possible at this time...)") + isExistingCard = false } document.getElementById("publish-card-form").reset() @@ -575,29 +582,6 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato blocksMinted } }) - //TODO verify this new voterPromises async function works better. - // const voterPromises = pollData.votes.map(async (vote) => { - // const voterPublicKey = vote.voterPublicKey; - // const voterAddress = await getAddressFromPublicKey(voterPublicKey); - - // const [nameInfo, addressInfo] = await Promise.all([ - // getNameFromAddress(voterAddress).catch(() => ""), - // getAddressInfo(voterAddress).catch(() => ({})), - // ]); - - // const voterName = nameInfo || (nameInfo === voterAddress ? "" : voterAddress); - // const blocksMinted = addressInfo?.blocksMinted || 0; - - // return { - // optionIndex: vote.optionIndex, - // voterPublicKey, - // voterAddress, - // voterName, - // isAdmin: adminAddresses.includes(voterAddress), - // isMinter: memberAddresses.includes(voterAddress), - // blocksMinted, - // }; - // }); const allVoters = await Promise.all(voterPromises) const yesVoters = [] @@ -774,35 +758,6 @@ const fetchCommentsForCard = async (cardIdentifier) => { } } -// display the comments on the card, with passed cardIdentifier to identify the card -------------- -// const displayComments = async (cardIdentifier) => { -// try { -// const comments = await fetchCommentsForCard(cardIdentifier); -// const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) - -// for (const comment of comments) { -// const commentDataResponse = await qortalRequest({ -// action: "FETCH_QDN_RESOURCE", -// name: comment.name, -// service: "BLOG_POST", -// identifier: comment.identifier, -// }) -// const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp) -// const commentHTML = ` -//
-//

${commentDataResponse.creator}:

-//

${commentDataResponse.content}

-//

${timestamp}

-//
-// ` -// commentsContainer.insertAdjacentHTML('beforeend', commentHTML) -// } - -// } catch (error) { -// console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) -// } -// } - const displayComments = async (cardIdentifier) => { try { const comments = await fetchCommentsForCard(cardIdentifier) @@ -902,6 +857,8 @@ const countComments = async (cardIdentifier) => { } } + + const createModal = (modalType='') => { if (document.getElementById(`${modalType}-modal`)) { return @@ -1039,6 +996,68 @@ const generateDarkPastelBackgroundBy = (name) => { return `hsl(${hue}, ${saturation}%, ${lightness}%)` } +const handleInviteMinter = async (minterName) => { + try { + const blockInfo = await getLatestBlockInfo() + const blockHeight = toString(blockInfo.height) + if (blockHeight <= MINTER_INVITE_BLOCK_HEIGHT) { + console.log(`block height is under the featureTrigger height`) + } + const minterAccountInfo = await getNameInfo(minterName) + const minterAddress = await minterAccountInfo.owner + const adminPublicKey = await getPublicKeyByName(userState.accountName) + console.log(`about to attempt group invite, minterAddress: ${minterAddress}, adminPublicKey: ${adminPublicKey}`) + const inviteTransaction = await createGroupInviteTransaction(minterAddress, adminPublicKey, 694, minterAddress, 864000, 0) + + // Step 2: Sign the transaction using qortalRequest + const signedTransaction = await qortalRequest({ + action: "SIGN_TRANSACTION", + unsignedBytes: inviteTransaction + }) + + // Step 3: Process the transaction + console.warn(`signed transaction`,signedTransaction) + const processResponse = await processTransaction(signedTransaction) + + if (processResponse?.status === "OK") { + alert(`${minterName} has been successfully invited!`) + } else { + alert("Failed to process the invite transaction.") + } + } catch (error) { + console.error("Error inviting minter:", error) + alert("Error inviting minter. Please try again.") + } +} + +const createInviteButtonHtml = (creator, cardIdentifier) => { + return ` +
+ +
+ ` +} + +const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => { + const latestBlockInfo = await getLatestBlockInfo() + const isBlockPassed = latestBlockInfo.height > MINTER_INVITE_BLOCK_HEIGHT + + if (adminYes >= 9 && userState.isMinterAdmin) { + const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier) + console.log(`admin votes over 9, creating invite button...`, adminYes) + return inviteButtonHtml + } + + return null +} + + // Create the overall Minter Card HTML ----------------------------------------------- const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => { const { header, content, links, creator, timestamp, poll } = cardData @@ -1056,6 +1075,9 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun createModal('links') createModal('poll-details') + const inviteButtonHtml = await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier) + const inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : '' + return `
@@ -1063,20 +1085,21 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun

${creator}

${header}

-
MINTER'S POST
+
USER'S POST
${content}
-
MINTER'S LINKS
+
USER'S LINKS
-
CURRENT RESULTS
+
CURRENT SUPPORT RESULTS
+ ${inviteHtmlAdd}
Admin Yes: ${adminYes} Admin No: ${adminNo} @@ -1092,7 +1115,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun Weight: ${totalNoWeight}
-
SUPPORT
${creator}
+
SUPPORT ACTION FOR
${creator}

(click COMMENTS button to open/close card comments)

diff --git a/assets/js/Q-Mintership.js b/assets/js/Q-Mintership.js index 657c162..4bcb04b 100644 --- a/assets/js/Q-Mintership.js +++ b/assets/js/Q-Mintership.js @@ -1,12 +1,12 @@ -const messageIdentifierPrefix = `mintership-forum-message`; -const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`; +const messageIdentifierPrefix = `mintership-forum-message` +const messageAttachmentIdentifierPrefix = `mintership-forum-attachment` // NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically. -let replyToMessageIdentifier = null; -let latestMessageIdentifiers = {}; // To keep track of the latest message in each room -let currentPage = 0; // Track current pagination page -let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once. +let replyToMessageIdentifier = null +let latestMessageIdentifiers = {} // To keep track of the latest message in each room +let currentPage = 0 // Track current pagination page +let existingIdentifiers = new Set() // Keep track of existing identifiers to not pull them more than once. let messagesById = {} let messageOrder =[] @@ -22,38 +22,38 @@ const storeMessageInMap = (msg) => { messagesById[msg.identifier] = msg // We will keep an array 'messageOrder' to store the messages and limit the size they take messageOrder.push({ identifier: msg.identifier, timestamp: msg.timestamp }) - messageOrder.sort((a, b) => a.timestamp - b.timestamp); + messageOrder.sort((a, b) => a.timestamp - b.timestamp) while (messageOrder.length > MAX_MESSAGES) { // Remove oldest from the front - const oldest = messageOrder.shift(); + const oldest = messageOrder.shift() // Delete from the map as well - delete messagesById[oldest.identifier]; + delete messagesById[oldest.identifier] } } function saveMessagesToLocalStorage() { try { - const data = { messagesById, messageOrder }; - localStorage.setItem("forumMessages", JSON.stringify(data)); - console.log("Saved messages to localStorage. Count:", messageOrder.length); + const data = { messagesById, messageOrder } + localStorage.setItem("forumMessages", JSON.stringify(data)) + console.log("Saved messages to localStorage. Count:", messageOrder.length) } catch (error) { - console.error("Error saving to localStorage:", error); + console.error("Error saving to localStorage:", error) } } function loadMessagesFromLocalStorage() { try { - const stored = localStorage.getItem("forumMessages"); + const stored = localStorage.getItem("forumMessages") if (!stored) { - console.log("No saved messages in localStorage."); + console.log("No saved messages in localStorage.") return; } const parsed = JSON.parse(stored); if (parsed.messagesById && parsed.messageOrder) { messagesById = parsed.messagesById; messageOrder = parsed.messageOrder; - console.log(`Loaded ${messageOrder.length} messages from localStorage.`); + console.log(`Loaded ${messageOrder.length} messages from localStorage.`) } } catch (error) { console.error("Error loading messages from localStorage:", error); @@ -61,40 +61,42 @@ function loadMessagesFromLocalStorage() { } if (localStorage.getItem("latestMessageIdentifiers")) { - latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers")); + latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers")) } document.addEventListener("DOMContentLoaded", async () => { - console.log("DOMContentLoaded fired!"); + console.log("DOMContentLoaded fired!") // --- GENERAL LINKS (MINTERSHIP-FORUM and MINTER-BOARD) --- - const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]'); + const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]') mintershipForumLinks.forEach(link => { link.addEventListener('click', async (event) => { - event.preventDefault(); + event.preventDefault() if (!userState.isLoggedIn) { - await login(); + await login() } await loadForumPage(); - loadRoomContent("general"); - startPollingForNewMessages(); - }); - }); + loadRoomContent("general") + startPollingForNewMessages() + createScrollToTopButton() + }) + }) - const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]'); + const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]') minterBoardLinks.forEach(link => { link.addEventListener("click", async (event) => { event.preventDefault(); if (!userState.isLoggedIn) { - await login(); + await login() } if (typeof loadMinterBoardPage === "undefined") { - console.log("loadMinterBoardPage not found, loading script dynamically..."); - await loadScript("./assets/js/MinterBoard.js"); + console.log("loadMinterBoardPage not found, loading script dynamically...") + await loadScript("./assets/js/MinterBoard.js") } - await loadMinterBoardPage(); - }); - }); + await loadMinterBoardPage() + createScrollToTopButton() + }) + }) // --- ADMIN CHECK --- await verifyUserIsAdmin(); @@ -115,74 +117,74 @@ document.addEventListener("DOMContentLoaded", async () => { } if (userState.isAdmin) { - console.log(`User is an Admin. Admin-specific buttons will remain visible.`); + console.log(`User is an Admin. Admin-specific buttons will remain visible.`) // DATA-BOARD Links for Admins - const minterDataBoardLinks = document.querySelectorAll('a[href="ADMINBOARD"]'); + const minterDataBoardLinks = document.querySelectorAll('a[href="ADMINBOARD"]') minterDataBoardLinks.forEach(link => { link.addEventListener("click", async (event) => { - event.preventDefault(); + event.preventDefault() if (!userState.isLoggedIn) { - await login(); + await login() } if (typeof loadAdminBoardPage === "undefined") { - console.log("loadAdminBoardPage function not found, loading script dynamically..."); - await loadScript("./assets/js/AdminBoard.js"); + console.log("loadAdminBoardPage function not found, loading script dynamically...") + await loadScript("./assets/js/AdminBoard.js") } - await loadAdminBoardPage(); - }); - }); + await loadAdminBoardPage() + }) + }) // TOOLS Links for Admins - const toolsLinks = document.querySelectorAll('a[href="TOOLS"]'); + const toolsLinks = document.querySelectorAll('a[href="TOOLS"]') toolsLinks.forEach(link => { link.addEventListener('click', async (event) => { - event.preventDefault(); + event.preventDefault() if (!userState.isLoggedIn) { - await login(); + await login() } if (typeof loadMinterAdminToolsPage === "undefined") { - console.log("loadMinterAdminToolsPage function not found, loading script dynamically..."); - await loadScript("./assets/js/AdminTools.js"); + console.log("loadMinterAdminToolsPage function not found, loading script dynamically...") + await loadScript("./assets/js/AdminTools.js") } - await loadMinterAdminToolsPage(); - }); - }); + await loadMinterAdminToolsPage() + }) + }) } else { - console.log("User is NOT an Admin. Removing admin-specific links."); + console.log("User is NOT an Admin. Removing admin-specific links.") // Remove all admin-specific links and their parents - const toolsLinks = document.querySelectorAll('a[href="TOOLS"], a[href="ADMINBOARD"]'); + const toolsLinks = document.querySelectorAll('a[href="TOOLS"], a[href="ADMINBOARD"]') toolsLinks.forEach(link => { - const buttonParent = link.closest('button'); - if (buttonParent) buttonParent.remove(); + const buttonParent = link.closest('button') + if (buttonParent) buttonParent.remove() - const cardParent = link.closest('.item.features-image'); - if (cardParent) cardParent.remove(); + const cardParent = link.closest('.item.features-image') + if (cardParent) cardParent.remove() - link.remove(); - }); + link.remove() + }) // Center the remaining card if it exists - const remainingCard = document.querySelector('.features7 .row .item.features-image'); + const remainingCard = document.querySelector('.features7 .row .item.features-image') if (remainingCard) { - remainingCard.classList.remove('col-lg-6', 'col-md-6'); - remainingCard.classList.add('col-12', 'text-center'); + remainingCard.classList.remove('col-lg-6', 'col-md-6') + remainingCard.classList.add('col-12', 'text-center') } } - console.log("All DOMContentLoaded tasks completed."); -}); + console.log("All DOMContentLoaded tasks completed.") +}) async function loadScript(src) { return new Promise((resolve, reject) => { - const script = document.createElement("script"); - script.src = src; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); + const script = document.createElement("script") + script.src = src + script.onload = resolve + script.onerror = reject + document.head.appendChild(script) + }) } @@ -200,14 +202,14 @@ const loadForumPage = async () => { if ((typeof userState.isAdmin === 'undefined') || (!userState.isAdmin)){ try { // Fetch and verify the admin status asynchronously - userState.isAdmin = await verifyUserIsAdmin(); + userState.isAdmin = await verifyUserIsAdmin() } catch (error) { - console.error('Error verifying admin status:', error); + console.error('Error verifying admin status:', error) userState.isAdmin = false; // Default to non-admin if there's an issue } } - const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`; + const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar` const isAdmin = userState.isAdmin; // Create the forum layout, including a header, sub-menu, and keeping the original background image: style="background-image: url('/assets/images/background.jpg');"> @@ -229,7 +231,7 @@ const loadForumPage = async () => {
- `; + ` document.body.appendChild(mainContent); @@ -252,61 +254,61 @@ const loadForumPage = async () => { // Function to add the pagination buttons and related control mechanisms ------------------------ const renderPaginationControls = (room, totalMessages, limit) => { - const paginationContainer = document.getElementById("pagination-container"); - if (!paginationContainer) return; + const paginationContainer = document.getElementById("pagination-container") + if (!paginationContainer) return - paginationContainer.innerHTML = ""; // Clear existing buttons + paginationContainer.innerHTML = "" // Clear existing buttons - const totalPages = Math.ceil(totalMessages / limit); + const totalPages = Math.ceil(totalMessages / limit) // Add "Previous" button if (currentPage > 0) { - const prevButton = document.createElement("button"); - prevButton.innerText = "Previous"; + const prevButton = document.createElement("button") + prevButton.innerText = "Previous" prevButton.addEventListener("click", () => { if (currentPage > 0) { - currentPage--; - loadMessagesFromQDN(room, currentPage, false); + currentPage-- + loadMessagesFromQDN(room, currentPage, false) } - }); - paginationContainer.appendChild(prevButton); + }) + paginationContainer.appendChild(prevButton) } // Add numbered page buttons for (let i = 0; i < totalPages; i++) { - const pageButton = document.createElement("button"); - pageButton.innerText = i + 1; - pageButton.className = i === currentPage ? "active-page" : ""; + const pageButton = document.createElement("button") + pageButton.innerText = i + 1 + pageButton.className = i === currentPage ? "active-page" : "" pageButton.addEventListener("click", () => { if (i !== currentPage) { - currentPage = i; - loadMessagesFromQDN(room, currentPage, false); + currentPage = i + loadMessagesFromQDN(room, currentPage, false) } - }); - paginationContainer.appendChild(pageButton); + }) + paginationContainer.appendChild(pageButton) } // Add "Next" button if (currentPage < totalPages - 1) { - const nextButton = document.createElement("button"); + const nextButton = document.createElement("button") nextButton.innerText = "Next"; nextButton.addEventListener("click", () => { if (currentPage < totalPages - 1) { - currentPage++; + currentPage++ loadMessagesFromQDN(room, currentPage, false); } - }); - paginationContainer.appendChild(nextButton); + }) + paginationContainer.appendChild(nextButton) } } // Main function to load the full content of the room, along with all main functionality ----------------------------------- const loadRoomContent = async (room) => { - const forumContent = document.getElementById("forum-content"); + const forumContent = document.getElementById("forum-content") if (!forumContent) { - console.error("Forum content container not found!"); - return; + console.error("Forum content container not found!") + return } if (userState.isAdmin) { @@ -333,7 +335,7 @@ const loadRoomContent = async (room) => {
- `; + ` // Add modal for image preview forumContent.insertAdjacentHTML( @@ -345,13 +347,13 @@ const loadRoomContent = async (room) => {
- `); + `) - initializeQuillEditor(); - setupModalHandlers(); - setupFileInputs(room); + initializeQuillEditor() + setupModalHandlers() + setupFileInputs(room) //TODO - maybe turn this into its own function and put it as a button? But for now it's fine to just load the latest message's position by default I think. - const latestId = latestMessageIdentifiers[room]?.latestIdentifier; + const latestId = latestMessageIdentifiers[room]?.latestIdentifier if (latestId) { const page = await findMessagePage(room, latestId, 10) currentPage = page; @@ -360,8 +362,8 @@ const loadRoomContent = async (room) => { } else{ await loadMessagesFromQDN(room, currentPage) } - ; -}; + +} // Initialize Quill editor //TODO check the updated editor init code // const initializeQuillEditor = () => { @@ -385,11 +387,11 @@ const loadRoomContent = async (room) => { const initializeQuillEditor = () => { - const editorContainer = document.querySelector('#editor'); + const editorContainer = document.querySelector('#editor') if (!editorContainer) { - console.error("Editor container not found!"); - return; + console.error("Editor container not found!") + return } new Quill('#editor', { @@ -409,7 +411,7 @@ new Quill('#editor', { ['clean'] ] } - }); + }) } @@ -418,66 +420,66 @@ new Quill('#editor', { const setupModalHandlers = () => { document.addEventListener("click", (event) => { if (event.target.classList.contains("inline-image")) { - const modal = document.getElementById("image-modal"); - const modalImage = document.getElementById("modal-image"); - const caption = document.getElementById("caption"); + const modal = document.getElementById("image-modal") + const modalImage = document.getElementById("modal-image") + const caption = document.getElementById("caption") - modalImage.src = event.target.src; - caption.textContent = event.target.alt; - modal.style.display = "block"; + modalImage.src = event.target.src + caption.textContent = event.target.alt + modal.style.display = "block" } - }); + }) document.getElementById("close-modal").addEventListener("click", () => { - document.getElementById("image-modal").style.display = "none"; - }); + document.getElementById("image-modal").style.display = "none" + }) window.addEventListener("click", (event) => { - const modal = document.getElementById("image-modal"); + const modal = document.getElementById("image-modal") if (event.target === modal) { - modal.style.display = "none"; + modal.style.display = "none" } - }); -}; + }) +} -let selectedImages = []; -let selectedFiles = []; -let multiResource = []; -let attachmentIdentifiers = []; +let selectedImages = [] +let selectedFiles = [] +let multiResource = [] +let attachmentIdentifiers = [] // Set up file input handling const setupFileInputs = (room) => { - const imageFileInput = document.getElementById('image-input'); - const previewContainer = document.getElementById('preview-container'); - const addToPublishButton = document.getElementById('add-images-to-publish-button'); - const fileInput = document.getElementById('file-input'); - const sendButton = document.getElementById('send-button'); + const imageFileInput = document.getElementById('image-input') + const previewContainer = document.getElementById('preview-container') + const addToPublishButton = document.getElementById('add-images-to-publish-button') + const fileInput = document.getElementById('file-input') + const sendButton = document.getElementById('send-button') - const attachmentID = generateAttachmentID(room); + const attachmentID = generateAttachmentID(room) imageFileInput.addEventListener('change', (event) => { - previewContainer.innerHTML = ''; - selectedImages = [...event.target.files]; + previewContainer.innerHTML = '' + selectedImages = [...event.target.files] - addToPublishButton.disabled = selectedImages.length === 0; + addToPublishButton.disabled = selectedImages.length === 0 selectedImages.forEach((file, index) => { - const reader = new FileReader(); + const reader = new FileReader() reader.onload = () => { - const img = document.createElement('img'); - img.src = reader.result; - img.alt = file.name; - img.style = "width: 100px; height: 100px; object-fit: cover; border: 1px solid #ccc; border-radius: 5px;"; + const img = document.createElement('img') + img.src = reader.result + img.alt = file.name + img.style = "width: 100px; height: 100px; object-fit: cover; border: 1px solid #ccc; border-radius: 5px;" - const removeButton = document.createElement('button'); - removeButton.innerText = 'Remove'; - removeButton.classList.add('remove-image-button'); + const removeButton = document.createElement('button') + removeButton.innerText = 'Remove' + removeButton.classList.add('remove-image-button') removeButton.onclick = () => { - selectedImages.splice(index, 1); - img.remove(); - removeButton.remove(); - addToPublishButton.disabled = selectedImages.length === 0; - }; + selectedImages.splice(index, 1) + img.remove() + removeButton.remove() + addToPublishButton.disabled = selectedImages.length === 0 + } const container = document.createElement('div') container.style = "display: flex; flex-direction: column; align-items: center; margin: 5px;" @@ -650,11 +652,11 @@ function clearInputs() { // Show success notification const showSuccessNotification = () => { const notification = document.createElement('div') - notification.innerText = "Message published successfully! Please wait for confirmation." + notification.innerText = "Successfully Published! Please note that messages will not display until after they are CONFIRMED, be patient!" notification.style.color = "green" notification.style.marginTop = "1em" document.querySelector(".message-input-section").appendChild(notification); - alert(`Successfully Published! Please note that messages will not display until after they are CONFIRMED, be patient!`) + // alert(`Successfully Published! Please note that messages will not display until after they are CONFIRMED, be patient!`) setTimeout(() => { notification.remove() @@ -1073,11 +1075,14 @@ const buildSingleAttachmentHtml = async (attachment, room) => { } else if (room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) { // const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`; - const decryptedBase64 = await fetchEncryptedImageBase64(attachment.service, attachment.name, attachment.identifier, attachment.mimeType) - const dataUrl = `data:image/${attachment.mimeType};base64,${decryptedBase64}` + // const decryptedBase64 = await fetchEncryptedImageBase64(attachment.service, attachment.name, attachment.identifier, attachment.mimeType) + // const dataUrl = `data:image/${attachment.mimeType};base64,${decryptedBase64}` + //${attachment.filename} + // above copied from removed html that is now created with fetchImageUrl TODO test this to ensure it works as expected. + const imageHtml = await loadInLineImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType, 'admins') return `
- ${attachment.filename} + ${imageHtml} @@ -1160,6 +1165,63 @@ const updatePaginationControls = async (room, limit) => { renderPaginationControls(room, totalMessages, limit) } +const createScrollToTopButton = () => { + if (document.getElementById('scrollToTopButton')) return + + const button = document.createElement('button') + button.id = 'scrollToTopButton' + + button.innerHTML = '↑' + + // Initial “not visible” state + button.style.display = 'none' + + button.style.position = 'fixed' + button.style.bottom = '3vh' + button.style.right = '3vw' + button.style.width = '9vw' + button.style.height = '9vw' + button.style.minWidth = '45px' + button.style.minHeight = '45px' + button.style.maxWidth = '60px' + button.style.maxHeight = '60px' + button.style.borderRadius = '50%' + button.style.backgroundColor = 'black' + button.style.color = 'white' + button.style.border = '2px solid white' + button.style.boxShadow = '0 0 15px rgba(0,0,0,0.5)' + button.style.cursor = 'pointer' + button.style.zIndex = '1000' + button.style.transition = 'opacity 0.3s ease, transform 0.3s ease' + button.style.fontSize = '5vw' + button.style.minFontSize = '18px' + button.style.maxFontSize = '30px' + + button.onclick = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + document.body.appendChild(button) + + const adjustFontSize = () => { + const computedStyle = window.getComputedStyle(button) + let sizePx = parseFloat(computedStyle.fontSize) + if (sizePx < 18) sizePx = 18 + if (sizePx > 30) sizePx = 30 + button.style.fontSize = sizePx + 'px' + } + adjustFontSize() + + window.addEventListener('resize', adjustFontSize) + + window.addEventListener('scroll', () => { + if (window.scrollY > 200) { + button.style.display = 'block' + } else { + button.style.display = 'none' + } + }) +} // Polling function to check for new messages without clearing existing ones diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js index acfbf26..613386d 100644 --- a/assets/js/QortalApi.js +++ b/assets/js/QortalApi.js @@ -227,6 +227,7 @@ const verifyUserIsAdmin = async () => { } + const verifyAddressIsAdmin = async (address) => { console.log('verifyAddressIsAdmin called') console.log('address:', address) @@ -857,17 +858,12 @@ const searchResourcesWithStatus = async (query, limit, status = 'local') => { } const getResourceMetadata = async (service, name, identifier) => { - console.log('getResourceMetadata called') - console.log('service:', service) - console.log('name:', name) - console.log('identifier:', identifier) try { const response = await fetch(`${baseUrl}/arbitrary/metadata/${service}/${name}/${identifier}`, { method: 'GET', headers: { 'accept': 'application/json' } }) const data = await response.json() - console.log('Fetched resource metadata:', data) return data } catch (error) { console.error('Error fetching resource metadata:', error) @@ -888,22 +884,35 @@ const fetchFileBase64 = async (service, name, identifier) => { } } -async function loadImageHtml(service, name, identifier, filename, mimeType) { +const loadInLineImageHtml = async (service, name, identifier, filename, mimeType, room='admins') => { + let isEncrypted = false + + if (room === 'admins'){ + isEncrypted = true + } + + if ((service === "MAIL_PRIVATE") && (room === 'admins')) { + service = "FILE_PRIVATE" + } + try { - const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}` - // Fetch the file as a blob - const response = await fetch(url) - // Convert the response to a Blob - const fileBlob = new Blob([response], { type: mimeType }) - // Create an Object URL from the Blob - const objectUrl = URL.createObjectURL(fileBlob) - // Use the Object URL as the image source + const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64` + + const response = await fetch(url,{ + method: 'GET', + headers: { 'accept': 'text/plain' } + }) + + const data64 = await response.text() + const decryptedBase64 = await decryptObject(data64) + const base64 = isEncrypted ? decryptedBase64 : data64 + const objectUrl = base64ToBlobUrl(base64, mimeType) const attachmentHtml = `
${filename}
` return attachmentHtml } catch (error) { - console.error("Error fetching the image:", error) + console.error("Error loading in-line image HTML:", error) } } @@ -932,18 +941,14 @@ const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeT throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`) } - // 2) Get the encrypted base64 text const encryptedBase64Data = await response.text() console.log("Fetched Encrypted Base64 Data:", encryptedBase64Data) - // 3) Decrypt => returns decrypted base64 const decryptedBase64 = await decryptObject(encryptedBase64Data) console.log("Decrypted Base64 Data:", decryptedBase64) - // 4) Convert that to a Blob const fileBlob = base64ToBlob(decryptedBase64, mimeType) - // 5) Save the file using qortalRequest await qortalRequest({ action: "SAVE_FILE", blob: fileBlob, @@ -978,7 +983,7 @@ const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeT error ) } - } +} /** @@ -999,6 +1004,18 @@ const base64ToBlob = (base64String, mimeType) => { // Create a blob from the Uint8Array return new Blob([bytes], { type: mimeType }) } + +const base64ToBlobUrl = (base64, mimeType) => { + const binary = atob(base64) + const array = [] + + for (let i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)) + } + + const blob = new Blob([new Uint8Array(array)], { type: mimeType }) + return URL.createObjectURL(blob) +} const fetchEncryptedImageBase64 = async (service, name, identifier, mimeType) => { @@ -1158,6 +1175,129 @@ const voteYesOnPoll = async (poll) => { }) } +// Qortal Transaction-related calls --------------------------------------------------------------------------- + +const processTransaction = async (rawTransaction) => { + try { + const response = await fetch(`${baseUrl}/transactions/process`, { + method: 'POST', + headers: { + 'Accept': 'text/plain', + 'X-API-VERSION': '2', + 'Content-Type': 'text/plain' + }, + body: rawTransaction + }) + + if (!response.ok) throw new Error(`Transaction processing failed: ${response.status}`) + + const result = await response.text() + console.log("Transaction successfully processed:", result) + return result + } catch (error) { + 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 = 864000, txGroupId = 0) => { + + try { + // Fetch account reference correctly + const accountInfo = await getAddressInfo(recipientAddress) + const accountReference = accountInfo.reference + + // Validate inputs before making the request + if (!adminPublicKey || !accountReference || !recipientAddress) { + throw new Error("Missing required parameters for group invite transaction.") + } + + const payload = { + timestamp: Date.now(), + reference: accountReference, + fee: 0.01, + txGroupId: txGroupId, + recipient: recipientAddress, + adminPublicKey: adminPublicKey, + groupId: groupId, + invitee: invitee || recipientAddress, + timeToLive: timeToLive + } + + console.log("Sending group invite transaction payload:", payload) + + const response = await fetch(`${baseUrl}/groups/invite`, { + method: 'POST', + headers: { + 'Accept': 'text/plain', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`) + } + + const rawTransaction = await response.text() + console.log("Raw unsigned transaction created:", rawTransaction) + return rawTransaction + } catch (error) { + console.error("Error creating group invite transaction:", error) + throw error + } +} + + +const getLatestBlockInfo = async () => { + try { + const response = await fetch(`${baseUrl}/blocks/last`, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to fetch last block data: ${response.status}`); + } + + const blockData = await response.json(); + + // Validate and ensure the structure matches the desired format + const formattedBlockData = { + signature: blockData.signature || "", + version: blockData.version || 0, + reference: blockData.reference || "", + transactionCount: blockData.transactionCount || 0, + totalFees: blockData.totalFees || "0", + transactionsSignature: blockData.transactionsSignature || "", + height: blockData.height || 0, + timestamp: blockData.timestamp || 0, + minterPublicKey: blockData.minterPublicKey || "", + minterSignature: blockData.minterSignature || "", + atCount: blockData.atCount || 0, + atFees: blockData.atFees || "0", + encodedOnlineAccounts: blockData.encodedOnlineAccounts || "", + onlineAccountsCount: blockData.onlineAccountsCount || 0, + minterAddress: blockData.minterAddress || "", + minterLevel: blockData.minterLevel || 0 + } + + console.log("Last Block Data:", formattedBlockData) + return formattedBlockData + + } catch (error) { + console.error("Error fetching last block data:", error) + return null + } +} + + + // export { // userState, // adminGroups, diff --git a/index.html b/index.html index 872f0b9..1e4bfbb 100644 --- a/index.html +++ b/index.html @@ -21,10 +21,6 @@ - @@ -68,7 +64,7 @@ - Q-Mintership Alpha v0.69b
+ Q-Mintership Alpha v0.71b
@@ -81,41 +77,17 @@ - - - -

- Mintership Details

+ Community Forum

Learn more about the Mintership concept, and why it was needed. The days of 'sponsorship' are a thing of the past on the Qortal Network. No more will there be the ability to self-sponsor. A new era of Qortal begins! Join the conversation with the other minters and admins here!

@@ -168,10 +140,10 @@
-

Minter Admin Tools

+

Admin Board

- Are you one of the initially selected Minter Admins? We have the tools here you need to create and approve GROUP_APPROVAL transactions, and communicate securely with your fellow admins. There is a private forum, and Minter Admin Tools section available for you!

- + Make decisions together with the other admins on the Admin Board. The admin board is a FULLY ENCRYPTED decision-making board for the Mintership Admins. This board allows publishing 'cards' just like the Minter Board, but with two types of cards. Check out the Admin Board Here!

+
@@ -197,6 +169,43 @@
+
+
+
+
+

+ v0.71beta 01-04-2025

+
+
+
+
+

+ NEW Feature- 'INVITE MINTER' - 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 FORUM

+

NEW Feature - 'ScrollToTop button' - 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.

+

Fixes - Admin Room image embeds - 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.

+

Various additional fixes and cleanup. More account detail features and the modification of the Minter Admin Tools section into an 'account details explorer' will be taking place over time.

+
+
+
+
+ +
+
+
+
+

+ v0.70beta 01-03-2025

+
+
+
+
+

+ A few update patches have been made, so this is a patch update to fix various issues.

+
+
+
+
+
@@ -450,7 +459,7 @@ - +