New invite minter feature - new scrollTotop button - fixed image embeds on Admin Room in Forum, various other fixes and cleanup.

This commit is contained in:
crowetic 2025-01-04 20:28:26 -08:00
parent 3bb28de2b0
commit 320dd34117
6 changed files with 511 additions and 270 deletions

View File

@ -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 {

View File

@ -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()

View File

@ -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 () => {
<div id="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">
<h3>Create or Update Your Minter Card</h3>
<h3>Create or Update Your Card</h3>
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
@ -45,6 +48,7 @@ const loadMinterBoardPage = async () => {
</div>
`
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 = `
// <div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
// <p><strong><u>${commentDataResponse.creator}</strong>:</p></u>
// <p>${commentDataResponse.content}</p>
// <p><i>${timestamp}</p></i>
// </div>
// `
// 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 `
<div id="invite-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button onclick="handleInviteMinter('${creator}')"
style="padding: 10px; background:rgb(0, 109, 76) ; color: white; border: dotted; cursor: pointer; border-radius: 5px;"
onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout="this.style.backgroundColor='rgba(7, 122, 101, 0.63) '"
>
Invite Minter
</button>
</div>
`
}
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 `
<div class="minter-card" style="background-color: ${BgColor}">
<div class="minter-card-header">
@ -1063,20 +1085,21 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="support-header"><h5>MINTER'S POST</h5></div>
<div class="support-header"><h5>USER'S POST</h5></div>
<div class="info">
${content}
</div>
<div class="support-header"><h5>MINTER'S LINKS</h5></div>
<div class="support-header"><h5>USER'S LINKS</h5></div>
<div class="info-links">
${linksHTML}
</div>
<div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
<div class="results-header support-header"><h5>CURRENT SUPPORT RESULTS</h5></div>
<div class="minter-card-results">
<button onclick="togglePollDetails('${cardIdentifier}')">Display Poll Details</button>
<div id="poll-details-${cardIdentifier}" style="display: none;">
${detailsHtml}
</div>
${inviteHtmlAdd}
<div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span>
@ -1092,7 +1115,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
<span class="total-no">Weight: ${totalNoWeight}</span>
</div>
</div>
<div class="support-header"><h5>SUPPORT</h5><h5 style="color: #ffae42;">${creator}</h5>
<div class="support-header"><h5>SUPPORT ACTION FOR </h5><h5 style="color: #ffae42;">${creator}</h5>
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
</div>
<div class="actions">

View File

@ -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 () => {
</div>
<div id="forum-content" class="forum-content"></div>
</div>
`;
`
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) => {
<button id="send-button" class="send-button">Publish</button>
</div>
</div>
`;
`
// Add modal for image preview
forumContent.insertAdjacentHTML(
@ -345,13 +347,13 @@ const loadRoomContent = async (room) => {
<div id="caption" class="caption"></div>
<button id="download-button" class="download-button">Download</button>
</div>
`);
`)
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}`
//<img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"/>
// 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 `
<div class="attachment">
<img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"/>
${imageHtml}
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
Save ${attachment.filename}
</button>
@ -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

View File

@ -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 = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`
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,

View File

@ -21,10 +21,6 @@
<link rel="stylesheet" href="assets/dropdown/css/style.css">
<link rel="stylesheet" href="assets/socicon/css/styles.css">
<link rel="stylesheet" href="assets/theme/css/style.css">
<!-- <link rel="preload" href="https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap"></noscript>
<link rel="preload" href="https://fonts.googleapis.com/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="https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript> -->
<link rel="preload" as="style" href="assets/mobirise/css/mbr-additional.css?v=U9lZDZ"><link rel="stylesheet" href="assets/mobirise/css/mbr-additional.css?v=U9lZDZ" type="text/css">
@ -68,7 +64,7 @@
<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 Alpha v0.69b<br></a></span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.71b<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>
@ -81,41 +77,17 @@
</section>
<!-- <section data-bs-version="5.1" class="header1 boldm5 cid-ttRnlSkg2R mbr-fullscreen mbr-parallax-background" id="header1-1">
<div class="mbr-overlay" style="opacity: 0.4; background-color: rgb(0, 0, 0);">
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="title-wrapper">
<h1 class="mbr-section-title mbr-fonts-style display-1">Q-Mintership Alpha</h1>
<p class="mbr-text mbr-fonts-style display-7">This is the initial 'alpha' of the Mintership Forum / Mintership tools that will be built into the final Q-Mintership app. This is a simplistic version built by crowetic that will offer a very simple communciations location, and the tools for the minter admins to accomplish the necessary GROUP_APPROVAL transactions. Scroll down for the currently available tools...&nbsp;</p>
<div class="mbr-section-btn"><a class="btn btn-primary display-4" href="index.html#features7-6"><span class="mobi-mbri mobi-mbri-arrow-down mbr-iconfont mbr-iconfont-btn"></span>
See more
</a></div>
</div>
</div>
</div>
</div>
</section> -->
<section data-bs-version="5.1" class="features7 boldm5 cid-ttRnAijqXt" id="features7-6">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-6 col-md-6 item features-image active">
<a class="item-link" href="TOOLS">
<a class="item-link" href="ADMINBOARD">
<div class="item-wrapper">
<img src="assets/images/mbr-1623x1082.jpg" alt="MA Tools" data-slide-to="1" data-bs-slide-to="1">
<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">
Minter Admin Tools</h2>
Admin Board</h2>
</div>
</div>
</a>
@ -146,7 +118,7 @@
<span class="mbr-iconfont mbr-iconfont-btn mbri-file" style="color:aliceblue;"></span>
</div>
<h3 class="mbr-section-title mbr-fonts-style display-2">
Mintership Details</h3>
Community Forum</h3>
<p class="mbr-text mbr-fonts-style display-7">
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!</p>
<div class="icon-wrapper"><a class="btn btn-primary display-4" style="-ms-flex-align: center;" href="MINTERSHIP-FORUM">FORUM</a></div>
@ -168,10 +140,10 @@
<div class="icon-wrapper">
<span class="mbr-iconfont mbr-iconfont-btn mbri-extension" style="color:aliceblue;"></span>
</div>
<h3 class="mbr-section-title mbr-fonts-style display-2">Minter Admin Tools</h3>
<h3 class="mbr-section-title mbr-fonts-style display-2">Admin Board</h3>
<p class="mbr-text mbr-fonts-style display-7">
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!</p>
<div class="icon-wrapper"><a class="btn btn-primary display-4" style="-ms-flex-align: center;" href="TOOLS">ADMIN TOOLS</a></div>
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!</p>
<div class="icon-wrapper"><a class="btn btn-primary display-4" style="-ms-flex-align: center;" href="ADMINBOARD">ADMIN BOARD</a></div>
</div>
</div>
</div>
@ -197,6 +169,43 @@
<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">
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">
@ -434,7 +443,7 @@
</div>
<a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.69beta</p>
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.71beta</p>
</a>
</div>
<div class="col-12 col-lg-6">
@ -450,7 +459,7 @@
<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>
<!-- <script src="./assets/smoothscroll/smooth-scroll.js"></script> -->
<script src="./assets/ytplayer/index.js"></script>
<script src="./assets/dropdown/js/navbar-dropdown.js"></script>
<script src="./assets/theme/js/script.js"></script>