diff --git a/.sync_b2a788d19481.db b/.sync_b2a788d19481.db index 258c51a..af7b50c 100644 Binary files a/.sync_b2a788d19481.db and b/.sync_b2a788d19481.db differ diff --git a/.sync_b2a788d19481.db-shm b/.sync_b2a788d19481.db-shm index 58083a8..96b9cc3 100644 Binary files a/.sync_b2a788d19481.db-shm and b/.sync_b2a788d19481.db-shm differ diff --git a/.sync_b2a788d19481.db-wal b/.sync_b2a788d19481.db-wal index 4b645d5..1e65b21 100644 Binary files a/.sync_b2a788d19481.db-wal and b/.sync_b2a788d19481.db-wal differ diff --git a/assets/css/forum-styles.css b/assets/css/forum-styles.css index 5bab08a..ae51c58 100644 --- a/assets/css/forum-styles.css +++ b/assets/css/forum-styles.css @@ -639,20 +639,20 @@ body { transition: transform 0.2s ease-in-out; } */ .minter-card{ - background-color: #1e1e2e; + background-color: #0c1314; flex: auto; display: flex; min-width: 22rem; /* max-width: 22rem; */ max-width: calc(30% - 3rem); color: #ffffff; - border: 1px solid #333; + border: 1px solid #6c8389; border-radius: 12px; padding: 1vh; min-height: 30vh; max-height: auto; margin: 1vh; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 10px rgba(207, 214, 255, 0.31); font-family: 'Arial', sans-serif; transition: transform 0.2s ease-in-out; flex-direction: column; @@ -961,7 +961,7 @@ body { color: #ffffff; border: none; border-radius: 8px; - padding: 1vh 1.7rem; + padding: 1vh 1.1rem; cursor: pointer; transition: background-color 0.3s; } @@ -975,7 +975,7 @@ body { color: #ffffff; border: none; border-radius: 8px; - padding: 1.0vh 1.7rem; + padding: 1.0vh 1.1rem; cursor: pointer; transition: background-color 0.3s; } diff --git a/assets/js/AdminBoard.js b/assets/js/AdminBoard.js index a466762..fba8169 100644 --- a/assets/js/AdminBoard.js +++ b/assets/js/AdminBoard.js @@ -242,9 +242,9 @@ const fetchAllEncryptedCards = async () => { // Fetch poll results const pollResults = await fetchPollResults(decryptedCardData.poll); const minterNameFromIdentifier = await extractCardsMinterName(card.identifier); - const commentCount = await getCommentCount(card.identifier); + const encryptedCommentCount = await getEncryptedCommentCount(card.identifier); // Generate final card HTML - const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, commentCount); + const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount); replaceEncryptedSkeleton(card.identifier, finalCardHTML); } catch (error) { console.error(`Error processing card ${card.identifier}:`, error); @@ -491,7 +491,7 @@ const publishEncryptedCard = async () => { } } -const getCommentCount = async (cardIdentifier) => { +const getEncryptedCommentCount = async (cardIdentifier) => { try { const response = await qortalRequest({ action: 'SEARCH_QDN_RESOURCES', @@ -692,26 +692,63 @@ const closeLinkDisplayModal = async () => { modalContent.src = ''; // Clear the iframe source } +// const processQortalLinkForRendering = 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}`; +// } +// } +// return link; // Return unchanged if not a Qortal link +// } + const processQortalLinkForRendering = 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; +}; + +async function getMinterAvatar(minterName) { + const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`; + + try { + const response = await fetch(avatarUrl, { method: 'HEAD' }); + if (response.ok) { + // Avatar exists, return the image HTML + return `User Avatar`; + } else { + // Avatar not found or no permission + return ''; + } + } catch (error) { + console.error('Error checking avatar availability:', error); + return ''; + } } // 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 = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`; - const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`; + const minterAvatar = await getMinterAvatar(minterName) + // const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`; + const creatorAvatar = await getMinterAvatar(creator) const linksHTML = links.map((link, index) => ` + - + -

Published by: ${creator} on ${formattedDate}

+

By: ${creator} - ${formattedDate}

`; } 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!

+
@@ -632,7 +698,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
-

Published by: ${creator} on ${formattedDate}

+

By: ${creator} - ${formattedDate}

`; } diff --git a/assets/js/Q-Mintership.js b/assets/js/Q-Mintership.js index a69c10c..1ce22ea 100644 --- a/assets/js/Q-Mintership.js +++ b/assets/js/Q-Mintership.js @@ -550,7 +550,7 @@ const showSuccessNotification = () => { // Generate unique attachment ID const generateAttachmentID = (room, fileIndex = null) => { - const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${Date.now()}` : `${messageAttachmentIdentifierPrefix}-${room}-${Date.now()}`; + const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${randomID()}` : `${messageAttachmentIdentifierPrefix}-${room}-${randomID()}`; return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID; }; @@ -750,7 +750,7 @@ const isMessageNew = (message, mostRecentMessage) => { const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => { const replyHtml = buildReplyHtml(message, fetchMessages); - const attachmentHtml = buildAttachmentHtml(message, room); + const attachmentHtml = buildAttachmentHtml(message, room); const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`; return ` @@ -770,123 +770,126 @@ const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => { - `; -}; + ` +} 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 `
In reply to: ${repliedMessage.name} ${repliedMessage.date}
${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 `
${attachment.filename}
- `; + ` + } else if + (room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) { + return fetchEncryptedImageHtml(attachment) + } else { - // Non-image attachment return `
- `; + ` } -}; +} 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}
- `; + ` 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 = `
${filename}
`; - - 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 = `
${filename}
`; + + 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 = `
${filename}
`; + + return attachmentHtml } + const renderData = async (service, name, identifier) => { console.log('renderData called'); console.log('service:', service);