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:
2025-01-04 20:28:26 -08:00
parent 3bb28de2b0
commit 320dd34117
6 changed files with 511 additions and 270 deletions

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,