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:
crowetic 2024-12-24 00:27:17 -08:00
parent 5b30fff84f
commit 0fc471a1b8
8 changed files with 400 additions and 186 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

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,65 +457,76 @@ 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;
}
let publishedMinterName = minterNameInput;
// If not topic mode, validate the user actually entered a valid Minter name
if (!isTopic) {
publishedMinterName = await validateMinterName(minterNameInput);
if (!publishedMinterName) {
alert(`Minter name invalid! Name input: ${minterNameInput} - please check the name and try again!`)
alert(`"${minterNameInput}" doesn't seem to be a valid Minter name. Please check or use topic mode.`);
return;
}
if (!isExistingEncryptedCard) {
if (existingCardMinterNames.includes(publishedMinterName)) {
const updateCard = confirm(`Minter Name: ${publishedMinterName} - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... `)
// 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 {
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 {
// 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",
@ -466,30 +538,36 @@ const publishEncryptedCard = async () => {
publicKeys: verifiedAdminPublicKeys
});
if (!isExistingEncryptedCard){
// Possibly create a poll if its a brand new card
if (!isExistingEncryptedCard) {
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ['Yes, No'],
pollOwnerAddress: userState.accountAddress,
pollDescription: `Admin Board Poll Published By ${userState.accountName}`,
pollOptions: ["Yes, No"],
pollOwnerAddress: userState.accountAddress
});
alert("Card and poll published successfully!");
existingCardMinterNames.push(`${publishedMinterName}`)
// 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,11 +606,10 @@ 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);
if (!base64CommentData) {
@ -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>
`;
`
}

View File

@ -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.
const testMode = true;
const cardIdentifierPrefix = "testMB-board-card";
const testMode = false;
const cardIdentifierPrefix = "Minter-board-card";
let isExistingCard = false;
let existingCardData = {};
let existingCardIdentifier = {};
@ -17,8 +17,8 @@ const loadMinterBoardPage = async () => {
// Add the "Minter Board" content
const mainContent = document.createElement("div");
const publishButtonColor = generateDarkPastelBackgroundBy("MinterBoardPublishButton")
const minterBoardNameColor = generateDarkPastelBackgroundBy(randomID)
const publishButtonColor = '#527c9d'
const minterBoardNameColor = '#527c9d'
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
@ -192,7 +192,8 @@ const loadCards = async () => {
const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML
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);
} catch (error) {
@ -636,9 +637,9 @@ const generateDarkPastelBackgroundBy = (name) => {
// 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 formattedDate = new Date(timestamp).toLocaleString();
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
// const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const avatarHtml = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => `

View File

@ -132,7 +132,7 @@ const loadForumPage = async () => {
}
}
if (typeof userState.isAdmin === 'undefined') {
if ((typeof userState.isAdmin === 'undefined') || (!userState.isAdmin)){
try {
// Fetch and verify the admin status asynchronously
userState.isAdmin = await verifyUserIsAdmin();
@ -244,6 +244,10 @@ const loadRoomContent = async (room) => {
return;
}
if (userState.isAdmin) {
await loadOrFetchAdminGroupsData()
}
// Set initial content
forumContent.innerHTML = `
<div class="room-content">
@ -258,7 +262,7 @@ const loadRoomContent = async (room) => {
<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>
<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>
<button id="send-button" class="send-button">Publish</button>
@ -381,6 +385,7 @@ const setupFileInputs = (room) => {
addToPublishButton.addEventListener('click', () => {
processSelectedImages(selectedImages, multiResource, room);
selectedImages = [];
imageFileInput.value = "";
addToPublishButton.disabled = true;
});
@ -424,12 +429,12 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
// Handle send message
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
const messageIdentifier = room === "admins"
? `${messageIdentifierPrefix}-${room}-e-${Date.now()}`
: `${messageIdentifierPrefix}-${room}-${Date.now()}`;
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
: `${messageIdentifierPrefix}-${room}-${randomID()}`;
const adminPublicKeys = room === "admins" && userState.isAdmin
? await fetchAdminGroupsMembersPublicKeys()
: [];
// const checkedAdminPublicKeys = room === "admins" && userState.isAdmin
// ? adminPublicKeys
// : await loadOrFetchAdminGroupsData().publicKeys;
try {
// Process selected images
@ -481,19 +486,19 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
service: "MAIL_PRIVATE",
identifier: messageIdentifier,
data64: base64Message,
});
})
} else {
multiResource.push({
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message,
});
})
}
// Publish resources
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.");
window.alert("You are not authorized to post in the Admin room.");
return;
@ -596,7 +601,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
let mostRecentMessage = getCurrentMostRecentMessage(room);
const fetchMessages = await fetchAllMessages(response, service, room);
const { firstNewMessageIdentifier, updatedMostRecentMessage } = renderNewMessages(
const { firstNewMessageIdentifier, updatedMostRecentMessage } = await renderNewMessages(
fetchMessages,
existingIdentifiers,
messagesContainer,
@ -655,10 +660,29 @@ const getCurrentMostRecentMessage = (room) => {
return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
};
// 1) Convert fetchAllMessages to fully async
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) => {
try {
// Skip if already displayed
@ -702,6 +726,7 @@ const fetchFullMessage = async (resource, service, room) => {
}
};
const processMessageObject = async (messageResponse, room) => {
if (room !== "admins") {
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 updatedMostRecentMessage = mostRecentMessage;
@ -727,7 +752,7 @@ const renderNewMessages = (fetchMessages, existingIdentifiers, messagesContainer
firstNewMessageIdentifier = message.identifier;
}
const messageHTML = buildMessageHTML(message, fetchMessages, room, isNewMessage);
const messageHTML = await buildMessageHTML(message, fetchMessages, room, isNewMessage);
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
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);
};
const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
const replyHtml = buildReplyHtml(message, fetchMessages);
const attachmentHtml = buildAttachmentHtml(message, room);
const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const replyHtml = await buildReplyHtml(message, fetchMessages);
const attachmentHtml = await buildAttachmentHtml(message, room);
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
return `
@ -773,7 +798,7 @@ const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
`
}
const buildReplyHtml = (message, fetchMessages) => {
const buildReplyHtml = async (message, fetchMessages) => {
if (!message.replyTo) return ""
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
@ -787,23 +812,47 @@ const buildReplyHtml = (message, fetchMessages) => {
`
}
const buildAttachmentHtml = (message, room) => {
if (!message.attachments || message.attachments.length === 0) return ""
const buildAttachmentHtml = async (message, room) => {
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/')) {
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`
return `
<div class="attachment">
<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>
`
} else if
(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 {
return `

View File

@ -810,88 +810,134 @@ const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeT
console.error("Filename and mimeType are required");
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") {
service = "FILE_PRIVATE";
}
const baseUrlWithParams = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?async=true&attempts=5`;
if (service === "FILE_PRIVATE") {
const urlPrivate = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64&async=true&attempts=5`
const response = await fetch(urlPrivate,{
// 1) We want the encrypted base64
const urlPrivate = `${baseUrlWithParams}&encoding=base64`;
const response = await fetch(urlPrivate, {
method: 'GET',
headers: { 'accept': 'text/plain' }
})
});
if (!response.ok) {
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`);
}
const encryptedBase64Data = response
console.log("Fetched Base64 Data:", encryptedBase64Data)
// 2) Get the encrypted base64 text
const encryptedBase64Data = await response.text();
console.log("Fetched Encrypted Base64 Data:", encryptedBase64Data);
// const sanitizedBase64 = encryptedBase64Data.replace(/[\r\n]+/g, '')
const decryptedData = await decryptObject(encryptedBase64Data)
console.log("Decrypted Data:", decryptedData);
// 3) Decrypt => returns decrypted base64
const decryptedBase64 = await decryptObject(encryptedBase64Data);
console.log("Decrypted Base64 Data:", decryptedBase64);
const fileBlob = new Blob((decryptedData), { type: mimeType })
// 4) Convert that to a Blob
const fileBlob = base64ToBlob(decryptedBase64, mimeType);
// 5) Save the file using qortalRequest
await qortalRequest({
action: "SAVE_FILE",
blob: fileBlob,
filename,
mimeType,
mimeType
});
console.log("Encrypted file saved successfully:", filename)
} else {
console.log("Encrypted file saved successfully:", filename);
const response = await fetch(url, {
} else {
// Normal, unencrypted file
const response = await fetch(baseUrlWithParams, {
method: 'GET',
headers: {'accept': 'text/plain'}
headers: { 'accept': 'text/plain' }
});
if (!response.ok) {
throw new Error(`File not found (HTTP ${response.status}): ${url}`)
throw new Error(`File not found (HTTP ${response.status}): ${baseUrlWithParams}`);
}
const blob = await response.blob()
const blob = await response.blob();
await qortalRequest({
action: "SAVE_FILE",
blob,
filename,
mimeType,
})
console.log("File saved successfully:", 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,{
/**
* 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) {
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
// Return null to "skip" the missing file
console.warn(`File not found (HTTP ${response.status}): ${urlPrivate}`);
return null;
}
//obtain the encrypted base64 of the image
const encryptedBase64Data = response
console.log("Fetched Base64 Data:", encryptedBase64Data)
//decrypt the encrypted base64 object
const decryptedData = await decryptObject(encryptedBase64Data)
console.log("Decrypted Data:", decryptedData);
//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 fileBlob = new Blob((decryptdData), { type: mimeType })
//create the URL for the decrypted file blob
const objectUrl = URL.createObjectURL(fileBlob)
//create the HTML from the file blob URL.
const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
return attachmentHtml
}
// 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) => {

View File

@ -68,7 +68,7 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a>
</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>
<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">
<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="row">
<div class="col-12 col-lg-7 card">
@ -208,7 +225,7 @@
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<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>
@ -281,7 +298,7 @@
</div>
<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>
</div>
<div class="col-12 col-lg-6">