Massive changes, all bugs have been fixed, Admin Board now allows 'topic mode' and 'name mode' to allow cards to be published with both. Admin Room in the forum now works with encrypted attachments and images, along with image preview and download button. One small issue remains with download button on image pop-up modal. But that will be fixed in patch release.

This commit is contained in:
2024-12-24 00:27:17 -08:00
parent 5b30fff84f
commit 0fc471a1b8
8 changed files with 400 additions and 186 deletions

View File

@@ -1,11 +1,14 @@
// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
const isEncryptedTestMode = true
const encryptedCardIdentifierPrefix = "test-MDC"
const encryptedCardIdentifierPrefix = "card-MAC"
let isExistingEncryptedCard = false
let existingDecryptedCardData = {}
let existingEncryptedCardIdentifier = {}
let cardMinterName = {}
let existingCardMinterNames = []
let isTopic = false
let attemptLoadAdminDataCount = 0
let adminMemberCount = 0
console.log("Attempting to load AdminBoard.js");
@@ -32,12 +35,16 @@ const loadAdminBoardPage = async () => {
<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>
<label for="minter-name-input">Minter Name:</label>
<input type="text" id="minter-name-input" maxlength="100" placeholder="Enter Minter's Name" required>
<div class="publish-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="topic-checkbox" name="topicMode" />
<label for="topic-checkbox">Is this a Topic instead of a Minter?</label>
</div>
<label for="minter-name-input">Input Topic or Minter Name:</label>
<input type="text" id="minter-name-input" maxlength="100" placeholder="Enter Topic or Minter's Name" required>
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Explain main point/issue" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter any information you like...You may also attach links to more in-depth information, etc." required></textarea>
<textarea id="card-content" placeholder="Enter any information you like...You may also attach links to more in-depth information, etc. (Links will pop-up in a custom in-app viewer upon being clicked.)" required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
@@ -90,11 +97,63 @@ const loadAdminBoardPage = async () => {
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishEncryptedCard();
const isTopicChecked = document.getElementById("topic-checkbox").checked;
// Pass that boolean to publishEncryptedCard
await publishEncryptedCard(isTopicChecked);
});
// await fetchAndValidateAllAdminCards();
await fetchAllEncryptedCards();
await updateOrSaveAdminGroupsDataLocally();
}
// Example: fetch and save admin public keys and count
const updateOrSaveAdminGroupsDataLocally = async () => {
try {
// Fetch the array of admin public keys
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys();
// Build an object containing the count and the array
const adminData = {
keysCount: verifiedAdminPublicKeys.length,
publicKeys: verifiedAdminPublicKeys
};
// Stringify and save to localStorage
localStorage.setItem('savedAdminData', JSON.stringify(adminData));
console.log('Admin public keys saved locally:', adminData);
} catch (error) {
console.error('Error fetching/storing admin public keys:', error);
attemptLoadAdminDataCount++
}
};
const loadOrFetchAdminGroupsData = async () => {
try {
// Pull the JSON from localStorage
const storedData = localStorage.getItem('savedAdminData')
if (!storedData && attemptLoadAdminDataCount <= 3) {
console.log('No saved admin public keys found in local storage. Fetching...')
await updateOrSaveAdminGroupsDataLocally()
attemptLoadAdminDataCount++
return null;
}
// Parse the JSON, then store the global variables.
const parsedData = JSON.parse(storedData)
adminMemberCount = parsedData.keysCount
adminPublicKeys = parsedData.publicKeys
console.log(`Loaded admins 'keysCount'=${adminMemberCount}, adminKeys=`, adminPublicKeys)
attemptLoadAdminDataCount = 0
return parsedData; // and return { adminMemberCount, adminKeys } to the caller
} catch (error) {
console.error('Error loading/parsing saved admin public keys:', error)
return null
}
}
const extractCardsMinterName = (cardIdentifier) => {
@@ -108,6 +167,10 @@ const extractCardsMinterName = (cardIdentifier) => {
if (parts.length < 3) {
throw new Error('Invalid identifier format');
}
if (parts.slice(2, -1).join('-') === 'TOPIC') {
console.log(`TOPIC found in identifier: ${cardIdentifier} - not including in duplicatesList`)
return
}
// Extract minterName (everything from the second part to the second-to-last part)
const minterName = parts.slice(2, -1).join('-');
// Return the extracted minterName
@@ -241,9 +304,10 @@ const fetchAllEncryptedCards = async () => {
// Fetch poll results
const pollResults = await fetchPollResults(decryptedCardData.poll);
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
// const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
// Generate final card HTML
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount);
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
} catch (error) {
@@ -386,9 +450,6 @@ const loadEncryptedCardIntoForm = async () => {
const validateMinterName = async(minterName) => {
try {
const nameInfo = await getNameInfo(minterName)
if (!nameInfo) {
return error (`No NameInfo able to be obtained? Did you pass name?`)
}
const name = nameInfo.name
return name
} catch (error){
@@ -396,66 +457,77 @@ const validateMinterName = async(minterName) => {
}
}
// Main function to publish a new Minter Card -----------------------------------------------
const publishEncryptedCard = async () => {
const publishEncryptedCard = async (isTopicModePassed = false) => {
// If the user wants it to be a topic, we set global isTopic = true, else false
isTopic = isTopicModePassed;
const minterNameInput = document.getElementById("minter-name-input").value.trim();
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
const publishedMinterName = await validateMinterName(minterNameInput)
// Basic validation
if (!header || !content) {
alert("Header and Content are required!");
return;
}
if (!publishedMinterName) {
alert(`Minter name invalid! Name input: ${minterNameInput} - please check the name and try again!`)
return;
}
let publishedMinterName = minterNameInput;
if (!isExistingEncryptedCard) {
if (existingCardMinterNames.includes(publishedMinterName)) {
const updateCard = confirm(`Minter Name: ${publishedMinterName} - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... `)
if (updateCard) {
await fetchExistingEncryptedCard(publishedMinterName)
await loadEncryptedCardIntoForm()
isExistingEncryptedCard = true
return
}else {
// If not topic mode, validate the user actually entered a valid Minter name
if (!isTopic) {
publishedMinterName = await validateMinterName(minterNameInput);
if (!publishedMinterName) {
alert(`"${minterNameInput}" doesn't seem to be a valid Minter name. Please check or use topic mode.`);
return;
}
// Also check for existing card if not topic
if (!isExistingEncryptedCard && existingCardMinterNames.includes(publishedMinterName)) {
const updateCard = confirm(
`Minter Name: ${publishedMinterName} already has a card. Update or Cancel?`
);
if (updateCard) {
await fetchExistingEncryptedCard(publishedMinterName);
await loadEncryptedCardIntoForm();
isExistingEncryptedCard = true;
return;
} else {
return;
}
}
}
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`;
const pollName = `${cardIdentifier}-poll`;
const pollDescription = `Admin Board Poll Published By ${userState.accountName}`;
// Determine final card identifier
const newCardIdentifier = isTopic
? `${encryptedCardIdentifierPrefix}-TOPIC-${await uid()}`
: `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`;
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : newCardIdentifier;
// Build cardData
const pollName = `${cardIdentifier}-poll`;
const cardData = {
minterName: `${publishedMinterName}`,
minterName: publishedMinterName,
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
topicMode: isTopic
};
try {
try {
// Convert to base64 or fallback
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData));
}
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
adminPublicKeys = verifiedAdminPublicKeys
const verifiedAdminPublicKeys = (adminPublicKeys) ? adminPublicKeys : loadOrFetchAdminGroupsData().publicKeys
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
@@ -466,30 +538,36 @@ const publishEncryptedCard = async () => {
publicKeys: verifiedAdminPublicKeys
});
if (!isExistingEncryptedCard){
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ['Yes, No'],
pollOwnerAddress: userState.accountAddress,
});
alert("Card and poll published successfully!");
existingCardMinterNames.push(`${publishedMinterName}`)
// Possibly create a poll if its a brand new card
if (!isExistingEncryptedCard) {
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription: `Admin Board Poll Published By ${userState.accountName}`,
pollOptions: ["Yes, No"],
pollOwnerAddress: userState.accountAddress
});
alert("Card and poll published successfully!");
// If its a real Minter name, store it so we know we have a card for them
if (!isTopic) {
existingCardMinterNames.push(publishedMinterName);
}
} else {
alert("Card updated successfully! (No poll updates possible currently...)");
}
if (isExistingEncryptedCard){
alert("Card Updated Successfully! (No poll updates are possible at this time...)")
}
// Cleanup UI
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("encrypted-cards-container").style.display = "flex";
isTopic = false; // reset global
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
}
}
};
const getEncryptedCommentCount = async (cardIdentifier) => {
try {
@@ -528,10 +606,9 @@ const postEncryptedComment = async (cardIdentifier) => {
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) {
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
const verifiedAdminPublicKeys = await loadOrFetchAdminGroupsData().publicKeys
adminPublicKeys = verifiedAdminPublicKeys
}
}
try {
const base64CommentData = await objectToBase64(commentData);
@@ -744,30 +821,54 @@ async function getMinterAvatar(minterName) {
// Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const minterAvatar = await getMinterAvatar(minterName)
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData
const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const creatorAvatar = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => `
<button onclick="openLinkDisplayModal('${link}')">
${`Link ${index + 1} - ${link}`}
</button>
`).join("");
`).join("")
const minterGroupMembers = await fetchMinterGroupMembers();
const minterAdmins = await fetchMinterGroupAdmins();
const isUndefinedUser = (minterName === 'undefined')
const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode')
// 2) Decide if this card is showing as "Topic" or "Name"
let showTopic = false
if (hasTopicMode) {
// If present, see if it's actually "true" or true
const modeVal = cardData.topicMode;
showTopic = (modeVal === true || modeVal === 'true')
} else {
if (!isUndefinedUser) {
// No topicMode => older card => default to Name
showTopic = false
}
}
const cardColorCode = showTopic ? '#0e1b15' : '#151f28'
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
<div class="support-header"><h5> REGARDING (Topic): </h5></div>
<h3>${minterName}</h3>` :
`
<div class="support-header"><h5> REGARDING (Name): </h5></div>
${minterAvatar}
<h3>${minterName}</h3>`
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculateAdminBoardPollResults(pollResults, minterGroupMembers, minterAdmins)
await createModal()
return `
<div class="admin-card">
<div class="admin-card" style="background-color: ${cardColorCode}">
<div class="minter-card-header">
<h2 class="support-header"> Created By: </h2>
${creatorAvatar}
<h2>${creator}</h2>
<div class="support-header"><h5> REGARDING: </h5></div>
${minterAvatar}
<h3>${minterName}</h3>
${minterOrTopicHtml}
<p>${header}</p>
</div>
<div class="info">
@@ -788,14 +889,14 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
<span class="minter-no">Denial Weight ${totalNoWeight}</span>
</div>
</div>
<div class="support-header"><h5>SUPPORT or DENY</h5><h5 style="color: #ffae42;">${minterName}</h5>
<div class="support-header"><h5>ACTIONS FOR</h5><h5 style="color: #ffae42;">${minterName}</h5>
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
</div>
<div class="actions">
<div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">SUPPORT</button>
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">OPPOSE</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div>
</div>
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
@@ -805,6 +906,6 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
</div>
<p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
</div>
`;
`
}