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:
@@ -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 it’s 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 it’s 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>
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user