massive modifications to both MinterBoard and forum. There are still many changes to come, but these needed to be pushed to resolve important bugs on MinterBoard for testing

This commit is contained in:
crowetic 2024-12-12 17:23:34 -08:00
parent f0a2a3ea58
commit a5bf12aed2
5 changed files with 460 additions and 201 deletions

View File

@ -169,6 +169,45 @@
padding: 1vh; padding: 1vh;
} }
.file-input {
display: none;
}
.image-input {
display: none;
}
/* Custom button styling */
.custom-file-input-button {
display: inline-block;
background-color: #07375a;
color: white;
padding: 0.5vh 0.2vh;
cursor: pointer;
border: solid white;
border-radius: 5px;
text-align: center;
}
.custom-file-input-button:hover {
background-color: #073783;
}
.custom-image-input-button {
display: inline-block;
background-color: #350550;
color: white;
padding: 0.5vh 0.2vh;
cursor: pointer;
border: solid white;
border-radius: 5px;
text-align: center;
}
.custom-image-input-button:hover {
background-color: #4d0541;
}
/* .ql-editor { /* .ql-editor {
flex-grow: 1; flex-grow: 1;
font-size: 1.25rem; font-size: 1.25rem;
@ -338,9 +377,9 @@
position: fixed; /* Stay in place */ position: fixed; /* Stay in place */
z-index: 1000; /* Sit on top */ z-index: 1000; /* Sit on top */
left: 10%; left: 10%;
top: 10%; top: 15%;
width: 80%; width: 80%;
height: 80%; height: 70%;
overflow: none; /* Enable scroll if needed */ overflow: none; /* Enable scroll if needed */
background-color: rgba(0, 0, 0, 0.8); /* Black w/ opacity */ background-color: rgba(0, 0, 0, 0.8); /* Black w/ opacity */
} }
@ -349,7 +388,7 @@
margin: auto; margin: auto;
display: block; display: block;
border: 2px dashed #638b93; border: 2px dashed #638b93;
width: 60%; width: 69%;
height: auto; height: auto;
max-width: 1200vh; max-width: 1200vh;
} }
@ -436,10 +475,12 @@ body {
/* Publish Card View */ /* Publish Card View */
.publish-card-view { .publish-card-view {
display: flex; display: flex;
flex-direction: column; text-align: left;
justify-content: center; /* Centers vertically */ padding: 0em;
align-items: center; /* Centers horizontally */ flex-wrap: wrap;
min-height: 80vh; /* Ensures the view takes full height of the viewport */ align-content: flex-start;
justify-content: center;
align-items: center;
} }

View File

@ -18,7 +18,7 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
}); });
async function loadMinterBoardPage() { const loadMinterBoardPage = async () => {
// Clear existing content on the page // Clear existing content on the page
const bodyChildren = document.body.children; const bodyChildren = document.body.children;
for (let i = bodyChildren.length - 1; i >= 0; i--) { for (let i = bodyChildren.length - 1; i >= 0; i--) {
@ -67,7 +67,7 @@ async function loadMinterBoardPage() {
if (updateCard) { if (updateCard) {
// Load existing card into the form for editing // Load existing card into the form for editing
loadCardIntoForm(existingCardData); await loadCardIntoForm(existingCardData);
alert("Edit your existing card and publish."); alert("Edit your existing card and publish.");
} else { } else {
// Allow creating a new card for testing purposes // Allow creating a new card for testing purposes
@ -91,14 +91,14 @@ async function loadMinterBoardPage() {
} }
}); });
document.getElementById("cancel-publish-button").addEventListener("click", () => { document.getElementById("cancel-publish-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container"); const cardsContainer = document.getElementById("cards-container");
cardsContainer.style.display = "flex"; // Restore visibility cardsContainer.style.display = "flex"; // Restore visibility
const publishCardView = document.getElementById("publish-card-view"); const publishCardView = document.getElementById("publish-card-view");
publishCardView.style.display = "none"; // Hide the publish form publishCardView.style.display = "none"; // Hide the publish form
}); });
document.getElementById("add-link-button").addEventListener("click", () => { document.getElementById("add-link-button").addEventListener("click", async () => {
const linksContainer = document.getElementById("links-container"); const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input"); const newLinkInput = document.createElement("input");
newLinkInput.type = "text"; newLinkInput.type = "text";
@ -115,7 +115,75 @@ async function loadMinterBoardPage() {
await loadCards(); await loadCards();
} }
async function fetchExistingCard() { //Main function to load the Minter Cards ----------------------------------------
const loadCards = async () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
mode: "ALL"
});
if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
const validatedCards = await Promise.all(
response.map(async card => {
console.log("Validating card:", card);
const isValid = await validateCardStructure(card);
return isValid ? card : null;
})
);
const validCards = validatedCards.filter(card => card !== null);
cardsContainer.innerHTML = "";
if (validCards.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>";
return;
}
await Promise.all(
validCards.map(async card => {
try {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
if (!cardData || !cardData.poll) {
console.warn(`Skipping card with missing poll data: ${JSON.stringify(cardData)}`);
return;
}
const pollResults = await fetchPollResults(cardData.poll);
console.log(`Poll Results Fetched - totalVotes: ${pollResults.totalVotes}`);
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error);
}
})
);
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
};
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
const fetchExistingCard = async () => {
try { try {
// Step 1: Perform the search // Step 1: Perform the search
const response = await qortalRequest({ const response = await qortalRequest({
@ -123,7 +191,8 @@ async function fetchExistingCard() {
service: "BLOG_POST", service: "BLOG_POST",
identifier: cardIdentifierPrefix, identifier: cardIdentifierPrefix,
name: userState.accountName, name: userState.accountName,
exactMatchNames: true //we want to search for the EXACT userName only when finding existing cards. mode: "ALL",
exactMatchNames: true // Search for the exact userName only when finding existing cards
}); });
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`); console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
@ -134,37 +203,48 @@ async function fetchExistingCard() {
return null; return null;
} }
const validCards = response.filter(card => validateCardStructure(card)); // Step 3: Validate cards asynchronously
const validatedCards = await Promise.all(
response.map(async card => {
const isValid = await validateCardStructure(card);
return isValid ? card : null;
})
);
// Step 4: Filter out invalid cards
const validCards = validatedCards.filter(card => card !== null);
if (validCards.length > 0) { if (validCards.length > 0) {
// Sort by most recent timestamp // Step 5: Sort by most recent timestamp
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]; const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0];
const cardDataResponse = await qortalRequest({ // Step 6: Fetch full card data
action: "FETCH_QDN_RESOURCE", const cardDataResponse = await qortalRequest({
name: userState.accountName, // User's account name action: "FETCH_QDN_RESOURCE",
service: "BLOG_POST", name: userState.accountName, // User's account name
identifier: mostRecentCard.identifier, service: "BLOG_POST",
}); identifier: mostRecentCard.identifier
});
existingCardIdentifier = mostRecentCard.identifier existingCardIdentifier = mostRecentCard.identifier;
existingCardData = cardDataResponse existingCardData = cardDataResponse;
console.log("Full card data fetched successfully:", cardDataResponse); console.log("Full card data fetched successfully:", cardDataResponse);
return cardDataResponse; // Return full card data return cardDataResponse;
} }
console.log("No valid cards found."); console.log("No valid cards found.");
return null; return null;
} catch (error) { } catch (error) {
console.error("Error fetching existing card:", error); console.error("Error fetching existing card:", error);
return null; return null;
} }
} };
const validateCardStructure = (card) => {
// Validate that a card is indeed a card and not a comment. -------------------------------------
const validateCardStructure = async (card) => {
return ( return (
typeof card === "object" && typeof card === "object" &&
card.name && card.name &&
@ -174,7 +254,9 @@ const validateCardStructure = (card) => {
); );
} }
function loadCardIntoForm(cardData) { // Load existing card data passed, into the form for editing -------------------------------------
const loadCardIntoForm = async (cardData) => {
console.log("Loading existing card data:", cardData);
document.getElementById("card-header").value = cardData.header; document.getElementById("card-header").value = cardData.header;
document.getElementById("card-content").value = cardData.content; document.getElementById("card-content").value = cardData.content;
@ -189,7 +271,8 @@ function loadCardIntoForm(cardData) {
}); });
} }
async function publishCard() { // Main function to publish a new Minter Card -----------------------------------------------
const publishCard = async () => {
const header = document.getElementById("card-header").value.trim(); const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim(); const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link")) const links = Array.from(document.querySelectorAll(".card-link"))
@ -213,7 +296,7 @@ async function publishCard() {
timestamp: Date.now(), timestamp: Date.now(),
poll: pollName, poll: pollName,
}; };
// new Date().toISOString()
try { try {
let base64CardData = await objectToBase64(cardData); let base64CardData = await objectToBase64(cardData);
@ -221,7 +304,6 @@ async function publishCard() {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData)); base64CardData = btoa(JSON.stringify(cardData));
} }
// const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({ await qortalRequest({
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -254,83 +336,45 @@ async function publishCard() {
} }
} }
async function loadCards() { //Calculate the poll results passed from other functions with minterGroupMembers and minterAdmins ---------------------------
const cardsContainer = document.getElementById("cards-container"); const calculatePollResults = async (pollData, minterGroupMembers, minterAdmins) => {
cardsContainer.innerHTML = "<p>Loading cards...</p>"; const memberAddresses = minterGroupMembers.map(member => member.member)
const adminAddresses = minterAdmins.map(member => member.member)
try { let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, yesWeight = 0 , noWeight = 0
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) { pollData.voteWeights.forEach(weightData => {
cardsContainer.innerHTML = "<p>No cards found.</p>"; if (weightData.optionName === 'Yes') {
return; yesWeight = weightData.voteWeight
} else if (weightData.optionName === 'No') {
noWeight = weightData.voteWeight
} }
})
cardsContainer.innerHTML = "" for (const vote of pollData.votes) {
const pollResultsCache = {}; const voterAddress = await getAddressFromPublicKey(vote.voterPublicKey)
console.log(`voter address: ${voterAddress}`)
const validCards = response.filter(card => validateCardStructure(card));
for (const card of validCards) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
if (!cardData || !cardData.poll) {
console.warn(`Skipping card with missing poll data: ${JSON.stringify(cardData)}`);
continue; // Skip to the next card
}
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
try {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
} catch (error) {
console.warn(`Failed to fetch poll results for poll: ${cardData.poll}`, error);
pollResultsCache[cardData.poll] = null; // Store as null to avoid repeated attempts
}
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
const calculatePollResults = (pollData, minterGroupMembers) => {
const memberAddresses = minterGroupMembers.map(member => member.member);
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
pollData.votes.forEach(vote => {
const voterAddress = getAddressFromPublicKey(vote.voterPublicKey);
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
if (vote.optionIndex === 0) { if (vote.optionIndex === 0) {
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null; adminAddresses.includes(voterAddress) ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`)
} else if (vote.optionIndex === 1) { } else if (vote.optionIndex === 1) {
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null; adminAddresses.includes(voterAddress) ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`)
} }
}); }
const totalYes = adminYes + minterYes; // TODO - create a new function to calculate the weights of each voting MINTER only.
const totalNo = adminNo + minterNo; // This will give ALL weight whether voter is in minter group or not...
// until that is changed on the core we must calculate manually.
const totalYesWeight = yesWeight
const totalNoWeight = noWeight
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo }; const totalYes = adminYes + minterYes
}; const totalNo = adminNo + minterNo
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo, totalYesWeight, totalNoWeight }
}
// Post a comment on a card. ---------------------------------
const postComment = async (cardIdentifier) => { const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`); const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
const commentText = commentInput.value.trim(); const commentText = commentInput.value.trim();
@ -353,7 +397,7 @@ const postComment = async (cardIdentifier) => {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CommentData = btoa(JSON.stringify(commentData)); base64CommentData = btoa(JSON.stringify(commentData));
} }
// const base64CommentData = btoa(JSON.stringify(commentData));
await qortalRequest({ await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE', action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName, name: userState.accountName,
@ -371,12 +415,14 @@ const postComment = async (cardIdentifier) => {
} }
}; };
//Fetch the comments for a card with passed card identifier ----------------------------
const fetchCommentsForCard = async (cardIdentifier) => { const fetchCommentsForCard = async (cardIdentifier) => {
try { try {
const response = await qortalRequest({ const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES', action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST', service: 'BLOG_POST',
query: `comment-${cardIdentifier}`, query: `comment-${cardIdentifier}`,
mode: "ALL"
}); });
return response; return response;
} catch (error) { } catch (error) {
@ -385,6 +431,7 @@ const fetchCommentsForCard = async (cardIdentifier) => {
} }
}; };
// display the comments on the card, with passed cardIdentifier to identify the card --------------
const displayComments = async (cardIdentifier) => { const displayComments = async (cardIdentifier) => {
try { try {
const comments = await fetchCommentsForCard(cardIdentifier); const comments = await fetchCommentsForCard(cardIdentifier);
@ -399,6 +446,7 @@ const displayComments = async (cardIdentifier) => {
identifier: comment.identifier, identifier: comment.identifier,
}); });
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp); const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp);
//TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section.
const commentHTML = ` const commentHTML = `
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;"> <div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
<p><strong><u>${commentDataResponse.creator}</strong>:</p></u> <p><strong><u>${commentDataResponse.creator}</strong>:</p></u>
@ -414,6 +462,7 @@ const displayComments = async (cardIdentifier) => {
} }
}; };
// Vote YES on a poll ------------------------------
const voteYesOnPoll = async (poll) => { const voteYesOnPoll = async (poll) => {
await qortalRequest({ await qortalRequest({
action: "VOTE_ON_POLL", action: "VOTE_ON_POLL",
@ -422,6 +471,7 @@ const voteYesOnPoll = async (poll) => {
}); });
} }
// Vote NO on a poll -----------------------------
const voteNoOnPoll = async (poll) => { const voteNoOnPoll = async (poll) => {
await qortalRequest({ await qortalRequest({
action: "VOTE_ON_POLL", action: "VOTE_ON_POLL",
@ -430,6 +480,7 @@ const voteNoOnPoll = async (poll) => {
}); });
} }
// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
const toggleComments = async (cardIdentifier) => { const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`); const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) { if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
@ -440,20 +491,58 @@ const toggleComments = async (cardIdentifier) => {
} }
}; };
async function createCardHTML(cardData, pollResults, cardIdentifier) { const createModal = async () => {
const modalHTML = `
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;">
<div style="position: relative; margin: 5% auto; width: 90%; height: 90%; background: white; border-radius: 10px; overflow: hidden;">
<iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe>
<button onclick="closeModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
// Function to open the modal
const openModal = async (link) => {
const processedLink = await processLink(link) // Process the link to replace `qortal://` for rendering in modal
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modalContent');
modalContent.src = processedLink; // Set the iframe source to the link
modal.style.display = 'block'; // Show the modal
}
// Function to close the modal
const closeModal = async () => {
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modalContent');
modal.style.display = 'none'; // Hide the modal
modalContent.src = ''; // Clear the iframe source
}
const processLink = async (link) => {
if (link.startsWith('qortal://')) {
return link.replace('qortal://', '/render/')
}
return link // Return the link unchanged if it doesn't start with `qortal://`
}
// Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
const { header, content, links, creator, timestamp, poll } = cardData; const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString(); const formattedDate = new Date(timestamp).toLocaleString();
const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`; const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const linksHTML = links.map((link, index) => ` const linksHTML = links.map((link, index) => `
<button onclick="window.open('${link}', '_blank')"> <button onclick="openModal('${link}')">
${`Link ${index + 1} - ${link}`} ${`Link ${index + 1} - ${link}`}
</button> </button>
`).join(""); `).join("");
const minterGroupMembers = await fetchMinterGroupMembers(); const minterGroupMembers = await fetchMinterGroupMembers();
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0 } = const minterAdmins = await fetchMinterGroupAdmins();
calculatePollResults(pollResults, minterGroupMembers) || {}; const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculatePollResults(pollResults, minterGroupMembers, minterAdmins)
await createModal()
return ` return `
<div class="minter-card"> <div class="minter-card">
<div class="minter-card-header"> <div class="minter-card-header">

View File

@ -14,7 +14,7 @@ if (localStorage.getItem("latestMessageIdentifiers")) {
} }
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum' // Identify the links for 'Mintership Forum' and apply functionality
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]'); const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => { mintershipForumLinks.forEach(link => {
@ -31,14 +31,9 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
}); });
async function loadForumPage() { // Main load function to clear existing HTML and load the forum page -----------------------------------------------------
// // Remove all sections except the menu const loadForumPage = async () => {
// const allSections = document.querySelectorAll('body > section'); // remove everything that isn't the menu from the body to use js to generate page content.
// allSections.forEach(section => {
// if (!section.classList.contains('menu')) {
// section.remove();
// }
// });
const bodyChildren = document.body.children; const bodyChildren = document.body.children;
for (let i = bodyChildren.length - 1; i >= 0; i--) { for (let i = bodyChildren.length - 1; i >= 0; i--) {
const child = bodyChildren[i]; const child = bodyChildren[i];
@ -89,7 +84,8 @@ async function loadForumPage() {
}); });
} }
async function renderPaginationControls(room, totalMessages, limit) { // Function to add the pagination buttons and related control mechanisms ------------------------
const renderPaginationControls = async(room, totalMessages, limit) => {
const paginationContainer = document.getElementById("pagination-container"); const paginationContainer = document.getElementById("pagination-container");
if (!paginationContainer) return; if (!paginationContainer) return;
@ -138,9 +134,8 @@ async function renderPaginationControls(room, totalMessages, limit) {
} }
} }
// Main function to load the full content of the room, along with all main functionality -----------------------------------
const loadRoomContent = async (room) => {
async function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content"); const forumContent = document.getElementById("forum-content");
if (forumContent) { if (forumContent) {
forumContent.innerHTML = ` forumContent.innerHTML = `
@ -153,9 +148,13 @@ async function loadRoomContent(room) {
<div id="editor" class="message-input"></div> <div id="editor" class="message-input"></div>
<div class="attachment-section"> <div class="attachment-section">
<input type="file" id="file-input" class="file-input" multiple> <input type="file" id="file-input" class="file-input" multiple>
<button id="attach-button" class="attach-button">Attach Files</button> <label for="file-input" class="custom-file-input-button">Select Files</label>
<input type="file" id="image-input" class="image-input" multiple accept="image/*">
<label for="image-input" class="custom-image-input-button">Select IMAGES w/Preview</label>
<button id="add-images-to-publish-button" disabled>Add Images to Multi-Publish</button>
<div id="preview-container" style="display: flex; flex-wrap: wrap; gap: 10px;"></div>
</div> </div>
<button id="send-button" class="send-button">Send</button> <button id="send-button" class="send-button">Publish</button>
</div> </div>
</div> </div>
`; `;
@ -191,7 +190,7 @@ async function loadRoomContent(room) {
// Load messages from QDN for the selected room // Load messages from QDN for the selected room
await loadMessagesFromQDN(room, currentPage); await loadMessagesFromQDN(room, currentPage);
document.addEventListener("click", (event) => { document.addEventListener("click", async (event) => {
if (event.target.classList.contains("inline-image")) { if (event.target.classList.contains("inline-image")) {
const modal = document.getElementById("image-modal"); const modal = document.getElementById("image-modal");
const modalImage = document.getElementById("modal-image"); const modalImage = document.getElementById("modal-image");
@ -202,72 +201,186 @@ async function loadRoomContent(room) {
modalImage.src = event.target.src; modalImage.src = event.target.src;
caption.textContent = event.target.alt; caption.textContent = event.target.alt;
// Set download button link - This has been moved to the Message Rendering Section of the code.
// downloadButton.onclick = () => {
// fetchAndSaveAttachment(
// "FILE",
// event.target.dataset.name,
// event.target.dataset.identifier,
// event.target.dataset.filename,
// event.target.dataset.mimeType
// );
// };
// Show the modal // Show the modal
modal.style.display = "block"; modal.style.display = "block";
} }
}); });
// Close the modal // Close the modal
document.getElementById("close-modal").addEventListener("click", () => { document.getElementById("close-modal").addEventListener("click", async () => {
document.getElementById("image-modal").style.display = "none"; document.getElementById("image-modal").style.display = "none";
}); });
// Hide the modal when clicking outside of the image or close button // Hide the modal when clicking outside of the image or close button
window.addEventListener("click", (event) => { window.addEventListener("click", async (event) => {
const modal = document.getElementById("image-modal"); const modal = document.getElementById("image-modal");
if (event.target == modal) { if (!event.target == modal) {
modal.style.display = "none"; modal.style.display = "none";
} }
}); });
let selectedFiles = []; let selectedFiles = [];
let selectedImages = [];
let attachmentIdentifiers = [];
let multiResource = []
// Add event listener to handle file selection const imageFileInput = document.getElementById('image-input');
document.getElementById('file-input').addEventListener('change', (event) => { const previewContainer = document.getElementById('preview-container');
selectedFiles = Array.from(event.target.files); const addToPublishButton = document.getElementById('add-images-to-publish-button')
const randomID = await uid();
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
imageFileInput.addEventListener('change', async (event) => {
// Clear previous previews to prepare for preview generation
previewContainer.innerHTML = '';
selectedImages = Array.from(event.target.files);
if (selectedImages.length > 0) {
addToPublishButton.disabled = false;
}
selectedImages.forEach((file, index) => {
const reader = new FileReader();
reader.onload = () => {
const img = document.createElement('img');
img.src = reader.result;
img.alt = file.name;
img.style.width = '100px';
img.style.height = '100px';
img.style.objectFit = 'cover';
img.style.border = '1px solid #ccc';
img.style.borderRadius = '5px';
// Add remove button
const removeButton = document.createElement('button');
removeButton.innerText = 'Remove';
removeButton.style.marginTop = '5px';
removeButton.onclick = () => {
selectedImages.splice(index, 1);
img.remove();
removeButton.remove();
if (selectedImages.length === 0) {
addToPublishButton.disabled = true;
}
};
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.margin = '5px';
container.appendChild(img);
container.appendChild(removeButton);
previewContainer.appendChild(container);
};
reader.readAsDataURL(file);
});
}); });
// Add event listener for the send button addToPublishButton.addEventListener('click', async () => {
await addImagesToMultiPublish()
})
// Function to add images in the preview to the multi-publish object --------------------------
const addImagesToMultiPublish = async () => {
console.log('Adding Images to multi-publish:', selectedImages);
for (let i = 0; i < selectedImages.length; i++) {
const file = selectedImages[i];
try {
multiResource.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
});
attachmentIdentifiers.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type
});
console.log(`Attachment ${file.name} placed into multiResource with attachmentID: ${attachmentID}`);
// Remove the processed file
selectedImages.splice(i, 1);
i--; // Adjust the index since we removed an item
} catch (error) {
console.error(`Error processing attachment ${file.name}:`, error);
}
}
selectedImages = []
addToPublishButton.disabled = true
}
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', async (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the PUBLISH button
document.getElementById("send-button").addEventListener("click", async () => { document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim(); const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) { if (messageHtml !== "" || selectedFiles.length > 0 || selectedImages.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`; const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments if (selectedImages.length > 0) {
for (const file of selectedFiles) { await addImagesToMultiPublish()
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`; }
try { if (selectedFiles.length === 1) {
await qortalRequest({ console.log(`single file has been detected, attaching single file...`)
action: "PUBLISH_QDN_RESOURCE", const singleAttachment = selectedFiles[0]
name: userState.accountName,
service: "FILE", multiResource.push({
identifier: attachmentID, name: userState.accountName,
file: file, service: "FILE",
}); identifier: attachmentID,
attachmentIdentifiers.push({ file: singleAttachment
name: userState.accountName, })
service: "FILE",
identifier: attachmentID, attachmentIdentifiers.push({
filename: file.name, name: userState.accountName,
mimeType: file.type service: "FILE",
}); identifier: attachmentID,
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`); filename: singleAttachment.name,
} catch (error) { filetype: singleAttachement.type
console.error(`Error publishing attachment ${file.name}:`, error); })
// Clear selectedFiles as we do not need them anymore.
document.getElementById('file-input').value = "";
selectedFiles = [];
}else if (selectedFiles.length >= 2) {
console.log(`selected files found: ${selectedFiles.length}, adding multiple files to multi-publish resource...`)
// Handle Multiple attachements utilizing multi-publish
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
try {
multiResource.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
});
attachmentIdentifiers.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type
});
console.log(`Attachment ${file.name} placed into multiResource with attachmentID: ${attachmentID}`);
// Remove the processed file
selectedFiles.splice(i, 1);
i--; // Adjust the index since we removed an item
} catch (error) {
console.error(`Error processing attachment ${file.name}:`, error);
}
} }
} }
@ -287,21 +400,24 @@ async function loadRoomContent(room) {
base64Message = btoa(JSON.stringify(messageObject)); base64Message = btoa(JSON.stringify(messageObject));
} }
// Publish message to QDN // Put the message into the multiResource for batch-publishing.
await qortalRequest({ multiResource.push({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, name: userState.accountName,
service: "BLOG_POST", service: "BLOG_POST",
identifier: messageIdentifier, identifier: messageIdentifier,
data64: base64Message data64: base64Message
}); });
console.log("Message published successfully"); console.log("Message added to multi-publish resource successfully, attempting multi-publish... ");
await publishMultipleResources(multiResource)
// Clear the editor after sending the message, including any potential attached files and replies. // Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = ""; quill.root.innerHTML = "";
document.getElementById('file-input').value = ""; document.getElementById('file-input').value = "";
selectedFiles = []; selectedFiles = [];
selectedImages = [];
multiResource = [];
replyToMessageIdentifier = null; replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container"); const replyContainer = document.querySelector(".reply-container");
if (replyContainer) { if (replyContainer) {
@ -310,33 +426,24 @@ async function loadRoomContent(room) {
// Show success notification // Show success notification
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show."; notification.innerText = "Message published successfully! Message will take a confirmation to show, please be patient...";
notification.style.color = "green"; notification.style.color = "green";
notification.style.marginTop = "10px"; notification.style.marginTop = "1em";
document.querySelector(".message-input-section").appendChild(notification); document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => { setTimeout(() => {
notification.remove(); notification.remove();
}, 3000); }, 10000);
} catch (error) { } catch (error) {
console.error("Error publishing message:", error); console.error("Error publishing message:", error);
} }
} }
}); })
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
} }
} }
async function loadMessagesFromQDN(room, page, isPolling = false) { const loadMessagesFromQDN = async (room, page, isPolling = false) => {
try { try {
const limit = 10; const limit = 10;
const offset = page * limit; const offset = page * limit;

View File

@ -130,17 +130,31 @@ const verifyUserIsAdmin = async () => {
try { try {
const accountAddress = userState.accountAddress || await getUserAddress(); const accountAddress = userState.accountAddress || await getUserAddress();
userState.accountAddress = accountAddress; userState.accountAddress = accountAddress;
const userGroups = await getUserGroups(accountAddress); const userGroups = await getUserGroups(accountAddress);
console.log('userGroups:', userGroups);
const minterGroupAdmins = await fetchMinterGroupAdmins(); const minterGroupAdmins = await fetchMinterGroupAdmins();
const isAdmin = await userGroups.some(group => adminGroups.includes(group.groupName)) console.log('minterGroupAdmins.members:', minterGroupAdmins);
const isMinterAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
if (!Array.isArray(userGroups)) {
throw new Error('userGroups is not an array or is undefined');
}
if (!Array.isArray(minterGroupAdmins)) {
throw new Error('minterGroupAdmins.members is not an array or is undefined');
}
const isAdmin = userGroups.some(group => adminGroups.includes(group.groupName));
const isMinterAdmin = minterGroupAdmins.some(admin => admin.member === userState.accountAddress && admin.isAdmin);
if (isMinterAdmin) { if (isMinterAdmin) {
userState.isMinterAdmin = true userState.isMinterAdmin = true;
} }
if (isAdmin) { if (isAdmin) {
userState.isAdmin = true; userState.isAdmin = true;
userState.isForumAdmin = true userState.isForumAdmin = true;
} }
return userState.isAdmin; return userState.isAdmin;
} catch (error) { } catch (error) {
console.error('Error verifying user admin status:', error); console.error('Error verifying user admin status:', error);
@ -148,6 +162,7 @@ const verifyUserIsAdmin = async () => {
} }
}; };
const verifyAddressIsAdmin = async (address) => { const verifyAddressIsAdmin = async (address) => {
console.log('verifyAddressIsAdmin called'); console.log('verifyAddressIsAdmin called');
console.log('address:', address); console.log('address:', address);
@ -234,8 +249,7 @@ const getAddressFromPublicKey = async (publicKey) => {
method: 'GET', method: 'GET',
headers: { 'Accept': 'text/plain' } headers: { 'Accept': 'text/plain' }
}); });
const data = await response.text(); const address = await response.text();
const address = data;
console.log('Converted Address:', address); console.log('Converted Address:', address);
return address; return address;
} catch (error) { } catch (error) {
@ -290,7 +304,7 @@ try {
}; };
// QORTAL GROUP-RELATED CALLS ------------------------------------------ // QORTAL GROUP-RELATED CALLS ------------------------------------------------------------------------------------
const getUserGroups = async (userAddress) => { const getUserGroups = async (userAddress) => {
console.log('getUserGroups called'); console.log('getUserGroups called');
console.log('userAddress:', userAddress); console.log('userAddress:', userAddress);
@ -319,8 +333,15 @@ const fetchMinterGroupAdmins = async () => {
headers: { 'Accept': 'application/json' } headers: { 'Accept': 'application/json' }
}); });
const admins = await response.json(); const admins = await response.json();
if (!Array.isArray(admins.members)) {
throw new Error("Expected 'members' to be an array but got a different structure");
}
const adminMembers = admins.members
console.log('Fetched minter admins', admins); console.log('Fetched minter admins', admins);
return admins; return adminMembers;
//use what is returned .member to obtain each member... {"member": "memberAddress", "isAdmin": "true"}
} }
const fetchMinterGroupMembers = async () => { const fetchMinterGroupMembers = async () => {
@ -341,7 +362,10 @@ const fetchMinterGroupMembers = async () => {
throw new Error("Expected 'members' to be an array but got a different structure"); throw new Error("Expected 'members' to be an array but got a different structure");
} }
return data.members; // Assuming 'members' is the key in the response JSON console.log(`MinterGroupMembers have been fetched.`)
return data.members;
//use what is returned .member to obtain each member... {"member": "memberAddress", "joined": "{timestamp}"}
} catch (error) { } catch (error) {
console.error("Error fetching minter group members:", error); console.error("Error fetching minter group members:", error);
return []; // Return an empty array to prevent further errors return []; // Return an empty array to prevent further errors
@ -349,8 +373,6 @@ const fetchMinterGroupMembers = async () => {
}; };
const fetchAllGroups = async (limit) => { const fetchAllGroups = async (limit) => {
console.log('fetchAllGroups called'); console.log('fetchAllGroups called');
console.log('limit:', limit); console.log('limit:', limit);

View File

@ -229,7 +229,7 @@
</div> </div>
<a class="link-wrap" href="#"> <a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.24beta</p> <p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.25beta</p>
</a> </a>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">