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:
parent
5b30fff84f
commit
0fc471a1b8
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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.
|
// 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 isEncryptedTestMode = true
|
||||||
const encryptedCardIdentifierPrefix = "test-MDC"
|
const encryptedCardIdentifierPrefix = "card-MAC"
|
||||||
let isExistingEncryptedCard = false
|
let isExistingEncryptedCard = false
|
||||||
let existingDecryptedCardData = {}
|
let existingDecryptedCardData = {}
|
||||||
let existingEncryptedCardIdentifier = {}
|
let existingEncryptedCardIdentifier = {}
|
||||||
let cardMinterName = {}
|
let cardMinterName = {}
|
||||||
let existingCardMinterNames = []
|
let existingCardMinterNames = []
|
||||||
|
let isTopic = false
|
||||||
|
let attemptLoadAdminDataCount = 0
|
||||||
|
let adminMemberCount = 0
|
||||||
|
|
||||||
console.log("Attempting to load AdminBoard.js");
|
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;">
|
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||||
<form id="publish-card-form">
|
<form id="publish-card-form">
|
||||||
<h3>Create or Update Your Minter Card</h3>
|
<h3>Create or Update Your Minter Card</h3>
|
||||||
<label for="minter-name-input">Minter Name:</label>
|
<div class="publish-card-checkbox" style="margin-top: 1em;">
|
||||||
<input type="text" id="minter-name-input" maxlength="100" placeholder="Enter Minter's Name" required>
|
<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>
|
<label for="card-header">Header:</label>
|
||||||
<input type="text" id="card-header" maxlength="100" placeholder="Explain main point/issue" required>
|
<input type="text" id="card-header" maxlength="100" placeholder="Explain main point/issue" required>
|
||||||
<label for="card-content">Content:</label>
|
<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>
|
<label for="card-links">Links (qortal://...):</label>
|
||||||
<div id="links-container">
|
<div id="links-container">
|
||||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
<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) => {
|
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await publishEncryptedCard();
|
const isTopicChecked = document.getElementById("topic-checkbox").checked;
|
||||||
|
|
||||||
|
// Pass that boolean to publishEncryptedCard
|
||||||
|
await publishEncryptedCard(isTopicChecked);
|
||||||
});
|
});
|
||||||
|
|
||||||
// await fetchAndValidateAllAdminCards();
|
// await fetchAndValidateAllAdminCards();
|
||||||
await fetchAllEncryptedCards();
|
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) => {
|
const extractCardsMinterName = (cardIdentifier) => {
|
||||||
@ -108,6 +167,10 @@ const extractCardsMinterName = (cardIdentifier) => {
|
|||||||
if (parts.length < 3) {
|
if (parts.length < 3) {
|
||||||
throw new Error('Invalid identifier format');
|
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)
|
// Extract minterName (everything from the second part to the second-to-last part)
|
||||||
const minterName = parts.slice(2, -1).join('-');
|
const minterName = parts.slice(2, -1).join('-');
|
||||||
// Return the extracted minterName
|
// Return the extracted minterName
|
||||||
@ -241,9 +304,10 @@ const fetchAllEncryptedCards = async () => {
|
|||||||
|
|
||||||
// Fetch poll results
|
// Fetch poll results
|
||||||
const pollResults = await fetchPollResults(decryptedCardData.poll);
|
const pollResults = await fetchPollResults(decryptedCardData.poll);
|
||||||
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
|
// const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
|
||||||
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
|
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
|
||||||
// Generate final card HTML
|
// Generate final card HTML
|
||||||
|
|
||||||
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount);
|
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount);
|
||||||
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
|
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -386,9 +450,6 @@ const loadEncryptedCardIntoForm = async () => {
|
|||||||
const validateMinterName = async(minterName) => {
|
const validateMinterName = async(minterName) => {
|
||||||
try {
|
try {
|
||||||
const nameInfo = await getNameInfo(minterName)
|
const nameInfo = await getNameInfo(minterName)
|
||||||
if (!nameInfo) {
|
|
||||||
return error (`No NameInfo able to be obtained? Did you pass name?`)
|
|
||||||
}
|
|
||||||
const name = nameInfo.name
|
const name = nameInfo.name
|
||||||
return name
|
return name
|
||||||
} catch (error){
|
} catch (error){
|
||||||
@ -396,66 +457,77 @@ const validateMinterName = async(minterName) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function to publish a new Minter Card -----------------------------------------------
|
const publishEncryptedCard = async (isTopicModePassed = false) => {
|
||||||
const publishEncryptedCard = async () => {
|
// 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 minterNameInput = document.getElementById("minter-name-input").value.trim();
|
||||||
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"))
|
||||||
.map(input => input.value.trim())
|
.map(input => input.value.trim())
|
||||||
.filter(link => link.startsWith("qortal://"));
|
.filter(link => link.startsWith("qortal://"));
|
||||||
const publishedMinterName = await validateMinterName(minterNameInput)
|
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
if (!header || !content) {
|
if (!header || !content) {
|
||||||
alert("Header and Content are required!");
|
alert("Header and Content are required!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!publishedMinterName) {
|
let publishedMinterName = minterNameInput;
|
||||||
alert(`Minter name invalid! Name input: ${minterNameInput} - please check the name and try again!`)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isExistingEncryptedCard) {
|
// If not topic mode, validate the user actually entered a valid Minter name
|
||||||
if (existingCardMinterNames.includes(publishedMinterName)) {
|
if (!isTopic) {
|
||||||
const updateCard = confirm(`Minter Name: ${publishedMinterName} - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... `)
|
publishedMinterName = await validateMinterName(minterNameInput);
|
||||||
if (updateCard) {
|
if (!publishedMinterName) {
|
||||||
await fetchExistingEncryptedCard(publishedMinterName)
|
alert(`"${minterNameInput}" doesn't seem to be a valid Minter name. Please check or use topic mode.`);
|
||||||
await loadEncryptedCardIntoForm()
|
|
||||||
isExistingEncryptedCard = true
|
|
||||||
return
|
|
||||||
}else {
|
|
||||||
return;
|
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()}`;
|
// Determine final card identifier
|
||||||
const pollName = `${cardIdentifier}-poll`;
|
const newCardIdentifier = isTopic
|
||||||
const pollDescription = `Admin Board Poll Published By ${userState.accountName}`;
|
? `${encryptedCardIdentifierPrefix}-TOPIC-${await uid()}`
|
||||||
|
: `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`;
|
||||||
|
|
||||||
|
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : newCardIdentifier;
|
||||||
|
|
||||||
|
// Build cardData
|
||||||
|
const pollName = `${cardIdentifier}-poll`;
|
||||||
const cardData = {
|
const cardData = {
|
||||||
minterName: `${publishedMinterName}`,
|
minterName: publishedMinterName,
|
||||||
header,
|
header,
|
||||||
content,
|
content,
|
||||||
links,
|
links,
|
||||||
creator: userState.accountName,
|
creator: userState.accountName,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
poll: pollName,
|
poll: pollName,
|
||||||
|
topicMode: isTopic
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert to base64 or fallback
|
||||||
let base64CardData = await objectToBase64(cardData);
|
let base64CardData = await objectToBase64(cardData);
|
||||||
if (!base64CardData) {
|
if (!base64CardData) {
|
||||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
|
||||||
base64CardData = btoa(JSON.stringify(cardData));
|
base64CardData = btoa(JSON.stringify(cardData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifiedAdminPublicKeys = (adminPublicKeys) ? adminPublicKeys : loadOrFetchAdminGroupsData().publicKeys
|
||||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
|
||||||
adminPublicKeys = verifiedAdminPublicKeys
|
|
||||||
|
|
||||||
await qortalRequest({
|
await qortalRequest({
|
||||||
action: "PUBLISH_QDN_RESOURCE",
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
name: userState.accountName,
|
name: userState.accountName,
|
||||||
@ -466,30 +538,36 @@ const publishEncryptedCard = async () => {
|
|||||||
publicKeys: verifiedAdminPublicKeys
|
publicKeys: verifiedAdminPublicKeys
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isExistingEncryptedCard){
|
// Possibly create a poll if it’s a brand new card
|
||||||
await qortalRequest({
|
if (!isExistingEncryptedCard) {
|
||||||
action: "CREATE_POLL",
|
await qortalRequest({
|
||||||
pollName,
|
action: "CREATE_POLL",
|
||||||
pollDescription,
|
pollName,
|
||||||
pollOptions: ['Yes, No'],
|
pollDescription: `Admin Board Poll Published By ${userState.accountName}`,
|
||||||
pollOwnerAddress: userState.accountAddress,
|
pollOptions: ["Yes, No"],
|
||||||
});
|
pollOwnerAddress: userState.accountAddress
|
||||||
|
});
|
||||||
alert("Card and poll published successfully!");
|
alert("Card and poll published successfully!");
|
||||||
existingCardMinterNames.push(`${publishedMinterName}`)
|
|
||||||
|
// 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){
|
// Cleanup UI
|
||||||
alert("Card Updated Successfully! (No poll updates are possible at this time...)")
|
|
||||||
}
|
|
||||||
document.getElementById("publish-card-form").reset();
|
document.getElementById("publish-card-form").reset();
|
||||||
document.getElementById("publish-card-view").style.display = "none";
|
document.getElementById("publish-card-view").style.display = "none";
|
||||||
document.getElementById("encrypted-cards-container").style.display = "flex";
|
document.getElementById("encrypted-cards-container").style.display = "flex";
|
||||||
|
isTopic = false; // reset global
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error publishing card or poll:", error);
|
console.error("Error publishing card or poll:", error);
|
||||||
alert("Failed to publish card and poll.");
|
alert("Failed to publish card and poll.");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const getEncryptedCommentCount = async (cardIdentifier) => {
|
const getEncryptedCommentCount = async (cardIdentifier) => {
|
||||||
try {
|
try {
|
||||||
@ -528,10 +606,9 @@ const postEncryptedComment = async (cardIdentifier) => {
|
|||||||
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
|
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
|
||||||
|
|
||||||
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) {
|
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) {
|
||||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
const verifiedAdminPublicKeys = await loadOrFetchAdminGroupsData().publicKeys
|
||||||
adminPublicKeys = verifiedAdminPublicKeys
|
adminPublicKeys = verifiedAdminPublicKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const base64CommentData = await objectToBase64(commentData);
|
const base64CommentData = await objectToBase64(commentData);
|
||||||
@ -744,30 +821,54 @@ async function getMinterAvatar(minterName) {
|
|||||||
|
|
||||||
// Create the overall Minter Card HTML -----------------------------------------------
|
// Create the overall Minter Card HTML -----------------------------------------------
|
||||||
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
||||||
const { minterName, header, content, links, creator, timestamp, poll } = cardData;
|
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData
|
||||||
const formattedDate = new Date(timestamp).toLocaleString();
|
const formattedDate = new Date(timestamp).toLocaleString()
|
||||||
const minterAvatar = await getMinterAvatar(minterName)
|
const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null
|
||||||
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
|
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
|
||||||
const creatorAvatar = await getMinterAvatar(creator)
|
const creatorAvatar = await getMinterAvatar(creator)
|
||||||
const linksHTML = links.map((link, index) => `
|
const linksHTML = links.map((link, index) => `
|
||||||
<button onclick="openLinkDisplayModal('${link}')">
|
<button onclick="openLinkDisplayModal('${link}')">
|
||||||
${`Link ${index + 1} - ${link}`}
|
${`Link ${index + 1} - ${link}`}
|
||||||
</button>
|
</button>
|
||||||
`).join("");
|
`).join("")
|
||||||
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers();
|
const isUndefinedUser = (minterName === 'undefined')
|
||||||
const minterAdmins = await fetchMinterGroupAdmins();
|
|
||||||
|
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)
|
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculateAdminBoardPollResults(pollResults, minterGroupMembers, minterAdmins)
|
||||||
await createModal()
|
await createModal()
|
||||||
return `
|
return `
|
||||||
<div class="admin-card">
|
<div class="admin-card" style="background-color: ${cardColorCode}">
|
||||||
<div class="minter-card-header">
|
<div class="minter-card-header">
|
||||||
<h2 class="support-header"> Created By: </h2>
|
<h2 class="support-header"> Created By: </h2>
|
||||||
${creatorAvatar}
|
${creatorAvatar}
|
||||||
<h2>${creator}</h2>
|
<h2>${creator}</h2>
|
||||||
<div class="support-header"><h5> REGARDING: </h5></div>
|
${minterOrTopicHtml}
|
||||||
${minterAvatar}
|
|
||||||
<h3>${minterName}</h3>
|
|
||||||
<p>${header}</p>
|
<p>${header}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@ -788,14 +889,14 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
|||||||
<span class="minter-no">Denial Weight ${totalNoWeight}</span>
|
<span class="minter-no">Denial Weight ${totalNoWeight}</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="actions-buttons">
|
<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="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>
|
</div>
|
||||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
<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>
|
</div>
|
||||||
<p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
|
<p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
|
// // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
|
||||||
const testMode = true;
|
const testMode = false;
|
||||||
const cardIdentifierPrefix = "testMB-board-card";
|
const cardIdentifierPrefix = "Minter-board-card";
|
||||||
let isExistingCard = false;
|
let isExistingCard = false;
|
||||||
let existingCardData = {};
|
let existingCardData = {};
|
||||||
let existingCardIdentifier = {};
|
let existingCardIdentifier = {};
|
||||||
@ -17,8 +17,8 @@ const loadMinterBoardPage = async () => {
|
|||||||
|
|
||||||
// Add the "Minter Board" content
|
// Add the "Minter Board" content
|
||||||
const mainContent = document.createElement("div");
|
const mainContent = document.createElement("div");
|
||||||
const publishButtonColor = generateDarkPastelBackgroundBy("MinterBoardPublishButton")
|
const publishButtonColor = '#527c9d'
|
||||||
const minterBoardNameColor = generateDarkPastelBackgroundBy(randomID)
|
const minterBoardNameColor = '#527c9d'
|
||||||
mainContent.innerHTML = `
|
mainContent.innerHTML = `
|
||||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||||
<h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
|
<h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
|
||||||
@ -192,7 +192,8 @@ const loadCards = async () => {
|
|||||||
const BgColor = generateDarkPastelBackgroundBy(card.name)
|
const BgColor = generateDarkPastelBackgroundBy(card.name)
|
||||||
// Generate final card HTML
|
// Generate final card HTML
|
||||||
const commentCount = await countComments(card.identifier)
|
const commentCount = await countComments(card.identifier)
|
||||||
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, BgColor);
|
const cardUpdatedTime = card.updated || null
|
||||||
|
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, BgColor);
|
||||||
|
|
||||||
replaceSkeleton(card.identifier, finalCardHTML);
|
replaceSkeleton(card.identifier, finalCardHTML);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -636,9 +637,9 @@ const generateDarkPastelBackgroundBy = (name) => {
|
|||||||
|
|
||||||
|
|
||||||
// Create the overall Minter Card HTML -----------------------------------------------
|
// Create the overall Minter Card HTML -----------------------------------------------
|
||||||
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, BgColor) => {
|
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => {
|
||||||
const { header, content, links, creator, timestamp, poll } = cardData;
|
const { header, content, links, creator, timestamp, poll } = cardData;
|
||||||
const formattedDate = new Date(timestamp).toLocaleString();
|
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
|
||||||
// const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
|
// const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
|
||||||
const avatarHtml = await getMinterAvatar(creator)
|
const avatarHtml = await getMinterAvatar(creator)
|
||||||
const linksHTML = links.map((link, index) => `
|
const linksHTML = links.map((link, index) => `
|
||||||
|
@ -132,7 +132,7 @@ const loadForumPage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof userState.isAdmin === 'undefined') {
|
if ((typeof userState.isAdmin === 'undefined') || (!userState.isAdmin)){
|
||||||
try {
|
try {
|
||||||
// Fetch and verify the admin status asynchronously
|
// Fetch and verify the admin status asynchronously
|
||||||
userState.isAdmin = await verifyUserIsAdmin();
|
userState.isAdmin = await verifyUserIsAdmin();
|
||||||
@ -244,6 +244,10 @@ const loadRoomContent = async (room) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userState.isAdmin) {
|
||||||
|
await loadOrFetchAdminGroupsData()
|
||||||
|
}
|
||||||
|
|
||||||
// Set initial content
|
// Set initial content
|
||||||
forumContent.innerHTML = `
|
forumContent.innerHTML = `
|
||||||
<div class="room-content">
|
<div class="room-content">
|
||||||
@ -258,7 +262,7 @@ const loadRoomContent = async (room) => {
|
|||||||
<label for="file-input" class="custom-file-input-button">Select Files</label>
|
<label for="file-input" class="custom-file-input-button">Select Files</label>
|
||||||
<input type="file" id="image-input" class="image-input" multiple accept="image/*">
|
<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>
|
<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>
|
<button id="add-images-to-publish-button" style="display: none" disabled>Add Images to Multi-Publish</button>
|
||||||
<div id="preview-container" style="display: flex; flex-wrap: wrap; gap: 10px;"></div>
|
<div id="preview-container" style="display: flex; flex-wrap: wrap; gap: 10px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<button id="send-button" class="send-button">Publish</button>
|
<button id="send-button" class="send-button">Publish</button>
|
||||||
@ -381,6 +385,7 @@ const setupFileInputs = (room) => {
|
|||||||
addToPublishButton.addEventListener('click', () => {
|
addToPublishButton.addEventListener('click', () => {
|
||||||
processSelectedImages(selectedImages, multiResource, room);
|
processSelectedImages(selectedImages, multiResource, room);
|
||||||
selectedImages = [];
|
selectedImages = [];
|
||||||
|
imageFileInput.value = "";
|
||||||
addToPublishButton.disabled = true;
|
addToPublishButton.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -424,12 +429,12 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
|
|||||||
// Handle send message
|
// Handle send message
|
||||||
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
|
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
|
||||||
const messageIdentifier = room === "admins"
|
const messageIdentifier = room === "admins"
|
||||||
? `${messageIdentifierPrefix}-${room}-e-${Date.now()}`
|
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
|
||||||
: `${messageIdentifierPrefix}-${room}-${Date.now()}`;
|
: `${messageIdentifierPrefix}-${room}-${randomID()}`;
|
||||||
|
|
||||||
const adminPublicKeys = room === "admins" && userState.isAdmin
|
// const checkedAdminPublicKeys = room === "admins" && userState.isAdmin
|
||||||
? await fetchAdminGroupsMembersPublicKeys()
|
// ? adminPublicKeys
|
||||||
: [];
|
// : await loadOrFetchAdminGroupsData().publicKeys;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process selected images
|
// Process selected images
|
||||||
@ -481,19 +486,19 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
|
|||||||
service: "MAIL_PRIVATE",
|
service: "MAIL_PRIVATE",
|
||||||
identifier: messageIdentifier,
|
identifier: messageIdentifier,
|
||||||
data64: base64Message,
|
data64: base64Message,
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
multiResource.push({
|
multiResource.push({
|
||||||
name: userState.accountName,
|
name: userState.accountName,
|
||||||
service: "BLOG_POST",
|
service: "BLOG_POST",
|
||||||
identifier: messageIdentifier,
|
identifier: messageIdentifier,
|
||||||
data64: base64Message,
|
data64: base64Message,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish resources
|
// Publish resources
|
||||||
if (room === "admins") {
|
if (room === "admins") {
|
||||||
if (!userState.isAdmin || adminPublicKeys.length === 0) {
|
if (!userState.isAdmin) {
|
||||||
console.error("User is not an admin or no admin public keys found. Aborting publish.");
|
console.error("User is not an admin or no admin public keys found. Aborting publish.");
|
||||||
window.alert("You are not authorized to post in the Admin room.");
|
window.alert("You are not authorized to post in the Admin room.");
|
||||||
return;
|
return;
|
||||||
@ -596,7 +601,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
|||||||
let mostRecentMessage = getCurrentMostRecentMessage(room);
|
let mostRecentMessage = getCurrentMostRecentMessage(room);
|
||||||
|
|
||||||
const fetchMessages = await fetchAllMessages(response, service, room);
|
const fetchMessages = await fetchAllMessages(response, service, room);
|
||||||
const { firstNewMessageIdentifier, updatedMostRecentMessage } = renderNewMessages(
|
const { firstNewMessageIdentifier, updatedMostRecentMessage } = await renderNewMessages(
|
||||||
fetchMessages,
|
fetchMessages,
|
||||||
existingIdentifiers,
|
existingIdentifiers,
|
||||||
messagesContainer,
|
messagesContainer,
|
||||||
@ -655,10 +660,29 @@ const getCurrentMostRecentMessage = (room) => {
|
|||||||
return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1) Convert fetchAllMessages to fully async
|
||||||
const fetchAllMessages = async (response, service, room) => {
|
const fetchAllMessages = async (response, service, room) => {
|
||||||
return Promise.all(response.map(resource => fetchFullMessage(resource, service, room)));
|
// Instead of returning Promise.all(...) directly,
|
||||||
|
// we explicitly map each resource to a try/catch block.
|
||||||
|
const messages = await Promise.all(
|
||||||
|
response.map(async (resource) => {
|
||||||
|
try {
|
||||||
|
const msg = await fetchFullMessage(resource, service, room);
|
||||||
|
return msg; // This might be null if you do that check in fetchFullMessage
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Skipping resource ${resource.identifier} due to error:`, err);
|
||||||
|
// Return null so it doesn't break everything
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out any that are null/undefined (missing or errored)
|
||||||
|
return messages.filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 2) fetchFullMessage is already async. We keep it async/await-based
|
||||||
const fetchFullMessage = async (resource, service, room) => {
|
const fetchFullMessage = async (resource, service, room) => {
|
||||||
try {
|
try {
|
||||||
// Skip if already displayed
|
// Skip if already displayed
|
||||||
@ -702,6 +726,7 @@ const fetchFullMessage = async (resource, service, room) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const processMessageObject = async (messageResponse, room) => {
|
const processMessageObject = async (messageResponse, room) => {
|
||||||
if (room !== "admins") {
|
if (room !== "admins") {
|
||||||
return messageResponse;
|
return messageResponse;
|
||||||
@ -716,7 +741,7 @@ const processMessageObject = async (messageResponse, room) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderNewMessages = (fetchMessages, existingIdentifiers, messagesContainer, room, mostRecentMessage) => {
|
const renderNewMessages = async (fetchMessages, existingIdentifiers, messagesContainer, room, mostRecentMessage) => {
|
||||||
let firstNewMessageIdentifier = null;
|
let firstNewMessageIdentifier = null;
|
||||||
let updatedMostRecentMessage = mostRecentMessage;
|
let updatedMostRecentMessage = mostRecentMessage;
|
||||||
|
|
||||||
@ -727,7 +752,7 @@ const renderNewMessages = (fetchMessages, existingIdentifiers, messagesContainer
|
|||||||
firstNewMessageIdentifier = message.identifier;
|
firstNewMessageIdentifier = message.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageHTML = buildMessageHTML(message, fetchMessages, room, isNewMessage);
|
const messageHTML = await buildMessageHTML(message, fetchMessages, room, isNewMessage);
|
||||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||||
|
|
||||||
if (!updatedMostRecentMessage || new Date(message.timestamp) > new Date(updatedMostRecentMessage?.latestTimestamp || 0)) {
|
if (!updatedMostRecentMessage || new Date(message.timestamp) > new Date(updatedMostRecentMessage?.latestTimestamp || 0)) {
|
||||||
@ -748,9 +773,9 @@ const isMessageNew = (message, mostRecentMessage) => {
|
|||||||
return !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
return !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
|
const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
||||||
const replyHtml = buildReplyHtml(message, fetchMessages);
|
const replyHtml = await buildReplyHtml(message, fetchMessages);
|
||||||
const attachmentHtml = buildAttachmentHtml(message, room);
|
const attachmentHtml = await buildAttachmentHtml(message, room);
|
||||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@ -773,7 +798,7 @@ const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildReplyHtml = (message, fetchMessages) => {
|
const buildReplyHtml = async (message, fetchMessages) => {
|
||||||
if (!message.replyTo) return ""
|
if (!message.replyTo) return ""
|
||||||
|
|
||||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
|
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
|
||||||
@ -787,23 +812,47 @@ const buildReplyHtml = (message, fetchMessages) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildAttachmentHtml = (message, room) => {
|
const buildAttachmentHtml = async (message, room) => {
|
||||||
if (!message.attachments || message.attachments.length === 0) return ""
|
if (!message.attachments || message.attachments.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
return message.attachments.map(attachment => buildSingleAttachmentHtml(attachment, room)).join("")
|
// Map over attachments -> array of Promises
|
||||||
}
|
const attachmentsHtmlPromises = message.attachments.map(attachment =>
|
||||||
|
buildSingleAttachmentHtml(attachment, room)
|
||||||
|
);
|
||||||
|
|
||||||
const buildSingleAttachmentHtml = (attachment, room) => {
|
// Wait for all Promises to resolve -> array of HTML strings
|
||||||
|
const attachmentsHtmlArray = await Promise.all(attachmentsHtmlPromises);
|
||||||
|
|
||||||
|
// Join them into a single string
|
||||||
|
return attachmentsHtmlArray.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSingleAttachmentHtml = async (attachment, room) => {
|
||||||
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
||||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`
|
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`
|
||||||
return `
|
return `
|
||||||
<div class="attachment">
|
<div class="attachment">
|
||||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
|
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
|
||||||
|
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
|
||||||
|
Save ${attachment.filename}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
} else if
|
} else if
|
||||||
(room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
(room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
||||||
return fetchEncryptedImageHtml(attachment)
|
// 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/png;base64,${decryptedBase64}`
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"/>
|
||||||
|
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
|
||||||
|
Save ${attachment.filename}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return `
|
return `
|
||||||
|
@ -806,92 +806,138 @@ async function loadImageHtml(service, name, identifier, filename, mimeType) {
|
|||||||
|
|
||||||
const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
|
const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
|
||||||
try {
|
try {
|
||||||
if (!filename || !mimeType) {
|
if (!filename || !mimeType) {
|
||||||
console.error("Filename and mimeType are required");
|
console.error("Filename and mimeType are required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?async=true&attempts=5`
|
|
||||||
|
// If it's a private file, we fetch with ?encoding=base64 and decrypt
|
||||||
if (service === "MAIL_PRIVATE") {
|
if (service === "MAIL_PRIVATE") {
|
||||||
service = "FILE_PRIVATE";
|
service = "FILE_PRIVATE";
|
||||||
}
|
}
|
||||||
if (service === "FILE_PRIVATE") {
|
|
||||||
const urlPrivate = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64&async=true&attempts=5`
|
const baseUrlWithParams = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?async=true&attempts=5`;
|
||||||
const response = await fetch(urlPrivate,{
|
|
||||||
method: 'GET',
|
if (service === "FILE_PRIVATE") {
|
||||||
headers: { 'accept': 'text/plain' }
|
// 1) We want the encrypted base64
|
||||||
})
|
const urlPrivate = `${baseUrlWithParams}&encoding=base64`;
|
||||||
if (!response.ok) {
|
const response = await fetch(urlPrivate, {
|
||||||
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
|
method: 'GET',
|
||||||
}
|
headers: { 'accept': 'text/plain' }
|
||||||
|
});
|
||||||
const encryptedBase64Data = response
|
|
||||||
console.log("Fetched Base64 Data:", encryptedBase64Data)
|
|
||||||
|
|
||||||
// const sanitizedBase64 = encryptedBase64Data.replace(/[\r\n]+/g, '')
|
|
||||||
const decryptedData = await decryptObject(encryptedBase64Data)
|
|
||||||
console.log("Decrypted Data:", decryptedData);
|
|
||||||
|
|
||||||
const fileBlob = new Blob((decryptedData), { type: mimeType })
|
|
||||||
|
|
||||||
await qortalRequest({
|
|
||||||
action: "SAVE_FILE",
|
|
||||||
blob: fileBlob,
|
|
||||||
filename,
|
|
||||||
mimeType,
|
|
||||||
});
|
|
||||||
console.log("Encrypted file saved successfully:", filename)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {'accept': 'text/plain'}
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`File not found (HTTP ${response.status}): ${url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob()
|
|
||||||
await qortalRequest({
|
|
||||||
action: "SAVE_FILE",
|
|
||||||
blob,
|
|
||||||
filename,
|
|
||||||
mimeType,
|
|
||||||
})
|
|
||||||
console.log("File saved successfully:", filename)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Error fetching or saving attachment (service: ${service}, name: ${name}, identifier: ${identifier}):`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchEncryptedImageHtml = async (service, name, identifier, filename, mimeType) => {
|
|
||||||
const urlPrivate = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64&async=true&attempts=5`
|
|
||||||
const response = await fetch(urlPrivate,{
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'accept': 'text/plain' }
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
|
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`);
|
||||||
}
|
}
|
||||||
//obtain the encrypted base64 of the image
|
|
||||||
const encryptedBase64Data = response
|
// 2) Get the encrypted base64 text
|
||||||
console.log("Fetched Base64 Data:", encryptedBase64Data)
|
const encryptedBase64Data = await response.text();
|
||||||
//decrypt the encrypted base64 object
|
console.log("Fetched Encrypted Base64 Data:", encryptedBase64Data);
|
||||||
const decryptedData = await decryptObject(encryptedBase64Data)
|
|
||||||
console.log("Decrypted Data:", decryptedData);
|
// 3) Decrypt => returns decrypted base64
|
||||||
//turn the decrypted object into a blob/uint8 array and specify mimetype //todo check if the uint8Array is needed or not. I am guessing not.
|
const decryptedBase64 = await decryptObject(encryptedBase64Data);
|
||||||
const fileBlob = new Blob((decryptdData), { type: mimeType })
|
console.log("Decrypted Base64 Data:", decryptedBase64);
|
||||||
//create the URL for the decrypted file blob
|
|
||||||
const objectUrl = URL.createObjectURL(fileBlob)
|
// 4) Convert that to a Blob
|
||||||
//create the HTML from the file blob URL.
|
const fileBlob = base64ToBlob(decryptedBase64, mimeType);
|
||||||
const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
|
|
||||||
|
// 5) Save the file using qortalRequest
|
||||||
|
await qortalRequest({
|
||||||
|
action: "SAVE_FILE",
|
||||||
|
blob: fileBlob,
|
||||||
|
filename,
|
||||||
|
mimeType
|
||||||
|
});
|
||||||
|
console.log("Encrypted file saved successfully:", filename);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Normal, unencrypted file
|
||||||
|
const response = await fetch(baseUrlWithParams, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'accept': 'text/plain' }
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`File not found (HTTP ${response.status}): ${baseUrlWithParams}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
await qortalRequest({
|
||||||
|
action: "SAVE_FILE",
|
||||||
|
blob,
|
||||||
|
filename,
|
||||||
|
mimeType
|
||||||
|
});
|
||||||
|
console.log("File saved successfully:", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error fetching or saving attachment (service: ${service}, name: ${name}, identifier: ${identifier}):`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return attachmentHtml
|
/**
|
||||||
}
|
* Convert a base64-encoded string into a Blob
|
||||||
|
* @param {string} base64String - The base64-encoded string (unencrypted)
|
||||||
|
* @param {string} mimeType - The MIME type of the file
|
||||||
|
* @returns {Blob} The resulting Blob
|
||||||
|
*/
|
||||||
|
const base64ToBlob = (base64String, mimeType) => {
|
||||||
|
// Decode base64 to binary string
|
||||||
|
const binaryString = atob(base64String);
|
||||||
|
// Convert binary string to Uint8Array
|
||||||
|
const len = binaryString.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
// Create a blob from the Uint8Array
|
||||||
|
return new Blob([bytes], { type: mimeType });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const fetchEncryptedImageBase64 = async (service, name, identifier, mimeType) => {
|
||||||
|
try {
|
||||||
|
// Fix potential typo: use &async=...
|
||||||
|
const urlPrivate = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64&async=true&attempts=5`;
|
||||||
|
|
||||||
|
const response = await fetch(urlPrivate, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'accept': 'text/plain' }
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
// Return null to "skip" the missing file
|
||||||
|
console.warn(`File not found (HTTP ${response.status}): ${urlPrivate}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Read the base64 text
|
||||||
|
const encryptedBase64Data = await response.text();
|
||||||
|
console.log("Fetched Encrypted Base64 Data:", encryptedBase64Data);
|
||||||
|
|
||||||
|
// 3) Decrypt => returns the *decrypted* base64 string
|
||||||
|
const decryptedBase64 = await decryptObject(encryptedBase64Data);
|
||||||
|
console.log("Decrypted Base64 Data:", decryptedBase64);
|
||||||
|
|
||||||
|
// 4) Convert that decrypted base64 into a Blob
|
||||||
|
const fileBlob = base64ToBlob(decryptedBase64, mimeType);
|
||||||
|
|
||||||
|
// 5) (Optional) Create an object URL
|
||||||
|
const objectUrl = URL.createObjectURL(fileBlob);
|
||||||
|
console.log("Object URL:", objectUrl);
|
||||||
|
|
||||||
|
// Return the base64 or objectUrl, whichever you need
|
||||||
|
return decryptedBase64;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Skipping file due to error in fetchEncryptedImageBase64:", error);
|
||||||
|
return null; // indicates "missing or failed"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const renderData = async (service, name, identifier) => {
|
const renderData = async (service, name, identifier) => {
|
||||||
|
23
index.html
23
index.html
@ -68,7 +68,7 @@
|
|||||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.56b<br></a></span>
|
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.6b<br></a></span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
|
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
|
||||||
|
|
||||||
@ -197,6 +197,23 @@
|
|||||||
|
|
||||||
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-7 card">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||||
|
Huge changes and fixes 12-23-2024</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-5 card">
|
||||||
|
<div class="text-wrapper">
|
||||||
|
<p class="mbr-text mbr-fonts-style display-7">
|
||||||
|
All of the Board and Forum bugs that were known have been resolved. There are still future changes to be made in order to add searching, etc. ALL IDENTIFIERS ARE SET TO PERMANENT IDENTIFIERS NOW. This means that all previous publishes that were done in testMode will no longer show up, and the app is now in 'officially released' status. Announcements about the MinterBoard can now be made publicly. Admin Board now allows publishing non-name-based cards with 'topics' instead, for use in admin voting. Encrypted images and attachments now work and download/display as they do in the unencrypted rooms on the forum. Much more to come, and much more has been done. </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-7 card">
|
<div class="col-12 col-lg-7 card">
|
||||||
@ -208,7 +225,7 @@
|
|||||||
<div class="col-12 col-lg-5 card">
|
<div class="col-12 col-lg-5 card">
|
||||||
<div class="text-wrapper">
|
<div class="text-wrapper">
|
||||||
<p class="mbr-text mbr-fonts-style display-7">
|
<p class="mbr-text mbr-fonts-style display-7">
|
||||||
The Minter and Admin boards received large updates today. Many modificatons. It should be about time to do public announcements about the Minter Board. New types of boards are in planning as well. The changes to the Mintership Forum Admins Room to allow encrypted file downloads are taking a bit longer than expected, but should be completed soon as well. Hopefully if all goes well the public announcements will take place on Monday. </a></p>
|
The Minter and Admin boards received large updates today. Many modificatons. It should be about time to do public announcements about the Minter Board. New types of boards are in planning as well. The changes to the Mintership Forum Admins Room to allow encrypted file downloads are taking a bit longer than expected, but should be completed soon as well. Hopefully if all goes well the public announcements will take place on Monday.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -281,7 +298,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.56beta</p>
|
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.6beta</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
|
Loading…
Reference in New Issue
Block a user