`;
}
diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js
index dca1909..ca0c064 100644
--- a/assets/js/MinterBoard.js
+++ b/assets/js/MinterBoard.js
@@ -17,11 +17,13 @@ const loadMinterBoardPage = async () => {
// Add the "Minter Board" content
const mainContent = document.createElement("div");
+ const publishButtonColor = generateDarkPastelBackgroundBy("MinterBoardPublishButton")
+ const minterBoardNameColor = generateDarkPastelBackgroundBy(randomID)
mainContent.innerHTML = `
-
Minter Board
-
The Minter Board is a place to publish information about yourself in order to obtain support from existing Minters and Minter Admins on the Qortal network. You may publish a header, content, and links to other QDN-published content in order to support you in your mission. Minter Admins and Existing Minters will then support you (or not) by way of a vote on your card. Card details you publish, along with existing poll results, and comments from others, will be displayed here. Good Luck on your Qortal journey to becoming a minter!
-
+
Minter Board
+
Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!
+
@@ -187,9 +189,11 @@ const loadCards = async () => {
// Fetch poll results
const pollResults = await fetchPollResults(cardDataResponse.poll);
-
+ const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML
- const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier);
+ const commentCount = await countComments(card.identifier)
+ const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, BgColor);
+
replaceSkeleton(card.identifier, finalCardHTML);
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error);
@@ -222,6 +226,7 @@ const createSkeletonCardHTML = (cardIdentifier) => {
return `
+
@@ -229,7 +234,7 @@ const createSkeletonCardHTML = (cardIdentifier) => {
-
+
PLEASE BE PATIENT
While data loads from QDN...
`;
@@ -530,6 +535,22 @@ const toggleComments = async (cardIdentifier) => {
}
};
+const countComments = async (cardIdentifier) => {
+ try {
+ const response = await qortalRequest({
+ action: 'SEARCH_QDN_RESOURCES',
+ service: 'BLOG_POST',
+ query: `comment-${cardIdentifier}`,
+ mode: "ALL"
+ });
+ // Just return the count; no need to decrypt each comment here
+ return Array.isArray(response) ? response.length : 0;
+ } catch (error) {
+ console.error(`Error fetching comment count for ${cardIdentifier}:`, error);
+ return 0;
+ }
+};
+
const createModal = async () => {
const modalHTML = `
@@ -563,22 +584,63 @@ const processLink = async (link) => {
if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
if (match) {
- const firstParam = match[1].toUpperCase(); // Convert to uppercase
- const remainingPath = match[2] || ""; // Rest of the URL
- // Perform any asynchronous operation if necessary
- await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
- return `/render/${firstParam}${remainingPath}`;
+ const firstParam = match[1].toUpperCase();
+ const remainingPath = match[2] || "";
+ const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined
+
+ // Simulating async operation if needed
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ // Append theme as a query parameter
+ return `/render/${firstParam}${remainingPath}?theme=${themeColor}`;
}
}
- return link; // Return unchanged if not a Qortal link
-}
+ return link;
+};
+
+// Hash the name and map it to a dark pastel color
+const generateDarkPastelBackgroundBy = (name) => {
+ // 1) Basic string hashing
+ let hash = 0;
+ for (let i = 0; i < name.length; i++) {
+ hash = (hash << 5) - hash + name.charCodeAt(i);
+ hash |= 0; // Convert to 32-bit integer
+ }
+ const safeHash = Math.abs(hash);
+
+ // 2) Restrict hue to a 'blue-ish' range (150..270 = 120 degrees total)
+ // We'll define a certain number of hue steps in that range.
+ const hueSteps = 69.69; // e.g., 12 steps
+ const hueIndex = safeHash % hueSteps;
+ // Each step is 120 / (hueSteps - 1) or so:
+ // but a simpler approach is 120 / hueSteps. It's okay if we don't use the extreme ends exactly.
+ const hueRange = 240;
+ const hue = 22.69 + (hueIndex * (hueRange / hueSteps));
+ // This yields values like 150, 160, 170, ... up to near 270
+
+ // 3) Saturation:
+ const satSteps = 13.69;
+ const satIndex = safeHash % satSteps;
+ const saturation = 18 + (satIndex * 1.333);
+
+ // 4) Lightness:
+ const lightSteps = 3.69;
+ const lightIndex = safeHash % lightSteps;
+ const lightness = 7 + lightIndex;
+
+ // 5) Return the HSL color string
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+};
+
+
// Create the overall Minter Card HTML -----------------------------------------------
-const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
+const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, BgColor) => {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
- const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
+ // const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
+ const avatarHtml = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => `
Reply
- `;
-};
+ `
+}
const buildReplyHtml = (message, fetchMessages) => {
- if (!message.replyTo) return "";
+ if (!message.replyTo) return ""
- const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
- if (!repliedMessage) return "";
+ const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
+ if (!repliedMessage) return ""
return `
${repliedMessage.content}
- `;
-};
+ `
+}
const buildAttachmentHtml = (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("");
-};
+ return message.attachments.map(attachment => buildSingleAttachmentHtml(attachment, room)).join("")
+}
const buildSingleAttachmentHtml = (attachment, room) => {
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 `
- `;
+ `
+ } else if
+ (room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
+ return fetchEncryptedImageHtml(attachment)
+
} else {
- // Non-image attachment
return `
Download ${attachment.filename}
- `;
+ `
}
-};
+}
const scrollToNewMessages = (firstNewMessageIdentifier) => {
- const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`);
+ const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`)
if (newMessageElement) {
- newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
-};
+}
const updateLatestMessageIdentifiers = (room, mostRecentMessage) => {
- latestMessageIdentifiers[room] = mostRecentMessage;
- localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
-};
+ latestMessageIdentifiers[room] = mostRecentMessage
+ localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers))
+}
const handleReplyLogic = (fetchMessages) => {
- const replyButtons = document.querySelectorAll(".reply-button");
+ const replyButtons = document.querySelectorAll(".reply-button")
replyButtons.forEach(button => {
button.addEventListener("click", () => {
- const replyToMessageIdentifier = button.dataset.messageIdentifier;
- const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
+ const replyToMessageIdentifier = button.dataset.messageIdentifier
+ const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier)
if (repliedMessage) {
- showReplyPreview(repliedMessage);
+ showReplyPreview(repliedMessage)
}
- });
- });
-};
+ })
+ })
+}
const showReplyPreview = (repliedMessage) => {
- replyToMessageIdentifier = repliedMessage.identifier;
+ replyToMessageIdentifier = repliedMessage.identifier
- const replyContainer = document.createElement("div");
- replyContainer.className = "reply-container";
+ const replyContainer = document.createElement("div")
+ replyContainer.className = "reply-container"
replyContainer.innerHTML = `
Replying to: ${repliedMessage.content}
Cancel
- `;
+ `
if (!document.querySelector(".reply-container")) {
- const messageInputSection = document.querySelector(".message-input-section");
+ const messageInputSection = document.querySelector(".message-input-section")
if (messageInputSection) {
- messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
+ messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild)
document.getElementById("cancel-reply").addEventListener("click", () => {
- replyToMessageIdentifier = null;
- replyContainer.remove();
- });
+ replyToMessageIdentifier = null
+ replyContainer.remove()
+ })
}
}
- const messageInputSection = document.querySelector(".message-input-section");
- const editor = document.querySelector(".ql-editor");
+ const messageInputSection = document.querySelector(".message-input-section")
+ const editor = document.querySelector(".ql-editor")
if (messageInputSection) {
- messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
if (editor) {
- editor.focus();
+ editor.focus()
}
-};
+}
const updatePaginationControls = async (room, limit) => {
- const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room)
- renderPaginationControls(room, totalMessages, limit);
-};
+ const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room)
+ renderPaginationControls(room, totalMessages, limit)
+}
// Polling function to check for new messages without clearing existing ones
function startPollingForNewMessages() {
setInterval(async () => {
- const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
+ const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0]
if (activeRoom) {
- await loadMessagesFromQDN(activeRoom, currentPage, true);
+ await loadMessagesFromQDN(activeRoom, currentPage, true)
}
- }, 40000);
+ }, 40000)
}
diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js
index a8206e7..8d4d4f6 100644
--- a/assets/js/QortalApi.js
+++ b/assets/js/QortalApi.js
@@ -28,9 +28,20 @@ const uid = async () => {
console.log('Generated uid:', result);
return result;
};
+// a non-async version of the uid function, in case non-async functions need it. Ultimately we can probably remove uid but need to ensure no apps are using it asynchronously first. so this is kept for that purpose for now.
+const randomID = () => {
+ console.log('randomID non-async');
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ const charactersLength = characters.length;
+ for (let i = 0; i < 6; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ };
+ console.log('Generated uid:', result);
+ return result;
+}
// Turn a unix timestamp into a human-readable date
const timestampToHumanReadableDate = async(timestamp) => {
- console.log('timestampToHumanReadableDate called');
const date = new Date(timestamp);
const day = date.getDate();
const month = date.getMonth() + 1;
@@ -45,7 +56,6 @@ const timestampToHumanReadableDate = async(timestamp) => {
};
// Base64 encode a string
const base64EncodeString = async (str) => {
- console.log('base64EncodeString called');
const encodedString = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(str).buffer)));
console.log('Encoded string:', encodedString);
return encodedString;
@@ -119,7 +129,6 @@ const userState = {
// USER-RELATED QORTAL CALLS ------------------------------------------
// Obtain the address of the authenticated user checking userState.accountAddress first.
const getUserAddress = async () => {
- console.log('getUserAddress called');
try {
if (userState.accountAddress) {
console.log('User address found in state:', userState.accountAddress);
@@ -778,57 +787,113 @@ const fetchFileBase64 = async (service, name, identifier) => {
async function loadImageHtml(service, name, identifier, filename, mimeType) {
try {
- const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
- // Fetch the file as a blob
- const response = await fetch(url);
- // Convert the response to a Blob
- const fileBlob = new Blob([response], { type: mimeType });
- // Create an Object URL from the Blob
- const objectUrl = URL.createObjectURL(fileBlob);
- // Use the Object URL as the image source
- const attachmentHtml = `
`;
-
- return attachmentHtml;
-
+ const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
+ // Fetch the file as a blob
+ const response = await fetch(url);
+ // Convert the response to a Blob
+ const fileBlob = new Blob([response], { type: mimeType });
+ // Create an Object URL from the Blob
+ const objectUrl = URL.createObjectURL(fileBlob);
+ // Use the Object URL as the image source
+ const attachmentHtml = `
`;
+
+ return attachmentHtml;
+
} catch (error) {
- console.error("Error fetching the image:", error);
+ console.error("Error fetching the image:", error);
}
}
const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
- const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
- if ((service === "FILE_PRIVATE") || (service === "MAIL_PRIVATE")) {
- service = "FILE_PRIVATE" || service
- try{
- const encryptedBase64Data = await fetchFileBase64(service, name, identifier)
- const decryptedBase64 = await decryptObject(encryptedBase64Data)
- const fileBlob = new Blob([decryptedBase64], { type: mimeType });
+ try {
+ if (!filename || !mimeType) {
+ console.error("Filename and mimeType are required");
+ return;
+ }
+ let url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?async=true&attempts=5`
+
+ if (service === "MAIL_PRIVATE") {
+ service = "FILE_PRIVATE";
+ }
+ if (service === "FILE_PRIVATE") {
+ 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}`)
+ }
+
+ 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
- });
-
- }catch (error) {
- console.error("Error fetching ro saving encrypted attachment",error)
- }
- }else{
- try {
- const response = await fetch(url);
- const blob = await response.blob();
- await qortalRequest({
- action: "SAVE_FILE",
- blob,
- filename: filename,
- mimeType
- });
- } catch (error) {
- console.error("Error fetching or saving the attachment:", error);
+ 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) {
+ throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
+ }
+ //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 = `
`;
+
+ return attachmentHtml
}
+
const renderData = async (service, name, identifier) => {
console.log('renderData called');
console.log('service:', service);