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.
|
||||
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 it’s 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 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,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>
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
|
@ -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) => `
|
||||
|
@ -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 `
|
||||
|
@ -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) => {
|
||||
|
23
index.html
23
index.html
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user