Bit updates to Minter and Admin boards. Still working on encrypted file downloads for the Admin Room on the forum... for whatever reason they're giving me a hassle. Just need a little more time to get it sorted.

Will also be adding additional use cases for the Admin board, and maybe a 'community board' as I really like the board concept for things like decision-making and community managmenent. I also have a really good idea for giving information on the boards via link modal concept. TBD. Will likely make the identifier changes and push announcements out on Monday. IF not sometime this weekend if time allows.
This commit is contained in:
crowetic 2024-12-20 22:07:18 -08:00
parent 57219987f3
commit 5e149039e9
8 changed files with 318 additions and 149 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -639,20 +639,20 @@ body {
transition: transform 0.2s ease-in-out; transition: transform 0.2s ease-in-out;
} */ } */
.minter-card{ .minter-card{
background-color: #1e1e2e; background-color: #0c1314;
flex: auto; flex: auto;
display: flex; display: flex;
min-width: 22rem; min-width: 22rem;
/* max-width: 22rem; */ /* max-width: 22rem; */
max-width: calc(30% - 3rem); max-width: calc(30% - 3rem);
color: #ffffff; color: #ffffff;
border: 1px solid #333; border: 1px solid #6c8389;
border-radius: 12px; border-radius: 12px;
padding: 1vh; padding: 1vh;
min-height: 30vh; min-height: 30vh;
max-height: auto; max-height: auto;
margin: 1vh; 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; font-family: 'Arial', sans-serif;
transition: transform 0.2s ease-in-out; transition: transform 0.2s ease-in-out;
flex-direction: column; flex-direction: column;
@ -961,7 +961,7 @@ body {
color: #ffffff; color: #ffffff;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 1vh 1.7rem; padding: 1vh 1.1rem;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
} }
@ -975,7 +975,7 @@ body {
color: #ffffff; color: #ffffff;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 1.0vh 1.7rem; padding: 1.0vh 1.1rem;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
} }

View File

@ -242,9 +242,9 @@ const fetchAllEncryptedCards = async () => {
// Fetch poll results // Fetch poll results
const pollResults = await fetchPollResults(decryptedCardData.poll); const pollResults = await fetchPollResults(decryptedCardData.poll);
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier); const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
const commentCount = await getCommentCount(card.identifier); const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
// Generate final card HTML // 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); replaceEncryptedSkeleton(card.identifier, finalCardHTML);
} catch (error) { } catch (error) {
console.error(`Error processing card ${card.identifier}:`, 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 { try {
const response = await qortalRequest({ const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES', action: 'SEARCH_QDN_RESOURCES',
@ -692,26 +692,63 @@ const closeLinkDisplayModal = async () => {
modalContent.src = ''; // Clear the iframe source 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) => { const processQortalLinkForRendering = async (link) => {
if (link.startsWith('qortal://')) { if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/); const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
if (match) { if (match) {
const firstParam = match[1].toUpperCase(); // Convert to uppercase const firstParam = match[1].toUpperCase();
const remainingPath = match[2] || ""; // Rest of the URL const remainingPath = match[2] || "";
// Perform any asynchronous operation if necessary const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined
await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
return `/render/${firstParam}${remainingPath}`; // 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 `<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">`;
} else {
// Avatar not found or no permission
return '';
}
} catch (error) {
console.error('Error checking avatar availability:', error);
return '';
}
} }
// Create the overall Minter Card HTML ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll } = cardData; const { minterName, header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString(); const formattedDate = new Date(timestamp).toLocaleString();
const minterAvatar = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`; const minterAvatar = await getMinterAvatar(minterName)
const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`; // const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const creatorAvatar = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => ` const linksHTML = links.map((link, index) => `
<button onclick="openLinkDisplayModal('${link}')"> <button onclick="openLinkDisplayModal('${link}')">
${`Link ${index + 1} - ${link}`} ${`Link ${index + 1} - ${link}`}
@ -725,50 +762,48 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
return ` return `
<div class="admin-card"> <div class="admin-card">
<div class="minter-card-header"> <div class="minter-card-header">
<h2 class="support-header"> Posted By:</h2> <h2 class="support-header"> Created By: </h2>
<img src="${creatorAvatar}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;"> ${creatorAvatar}
<h2>${creator}</h2> <h2>${creator}</h2>
<div class="support-header"><h5> Regarding Minter: </h5> <div class="support-header"><h5> REGARDING: </h5></div>
<img src="${minterAvatar}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;"> ${minterAvatar}
<h3>${minterName}</h3> <h3>${minterName}</h3>
<p>${header}</p> <p>${header}</p>
</div> </div>
<div class="info"> <div class="info">
${content} ${content}
</div> </div>
<div class="support-header"><h5>Informational Links:</h5></div> <div class="support-header"><h5>LINKS</h5></div>
<div class="info-links"> <div class="info-links">
${linksHTML} ${linksHTML}
</div> </div>
<div class="results-header support-header"><h5>Resulting Support:</h5></div> <div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
<div class="minter-card-results"> <div class="minter-card-results">
<div class="admin-results"> <div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span> <span class="admin-yes">Admin Support: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span> <span class="admin-no">Admin Against: ${adminNo}</span>
</div> </div>
<div class="minter-results"> <div class="minter-results">
<span class="minter-yes">TBD ${minterYes}</span> <span class="minter-yes">Supporting Weight ${totalYesWeight}</span>
<span class="minter-no">TBD ${minterNo}</span> <span class="minter-no">Denial Weight ${totalNoWeight}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-no">Total No: ${totalNo}</span>
</div> </div>
</div> </div>
<div class="support-header"><h5>Support ${minterName}?</h5></div> <div class="support-header"><h5>SUPPORT or DENY</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">
<div class="actions-buttons"> <div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button> <button class="yes" onclick="voteYesOnPoll('${poll}')">SUPPORT</button>
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button> <button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">OPPOSE</button>
</div> </div>
</div> </div>
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;"> <div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div> <div id="comments-container-${cardIdentifier}" class="comments-container"></div>
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea> <textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postEncryptedComment('${cardIdentifier}')">Post Comment</button> <button onclick="postEncryptedComment('${cardIdentifier}')">Post Comment</button>
</div> </div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p> <p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
</div> </div>
`; `;
} }

View File

@ -17,11 +17,13 @@ const loadMinterBoardPage = async () => {
// Add the "Minter Board" content // Add the "Minter Board" content
const mainContent = document.createElement("div"); const mainContent = document.createElement("div");
const publishButtonColor = generateDarkPastelBackgroundBy("MinterBoardPublishButton")
const minterBoardNameColor = generateDarkPastelBackgroundBy(randomID)
mainContent.innerHTML = ` mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;"> <div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1> <h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
<p style="font-size: 1.25em;"> 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!</p> <p style="font-size: 1.25em;"> Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button> <button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px; background-color: ${publishButtonColor}">Publish Minter Card</button>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button> <button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div> <div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
@ -187,9 +189,11 @@ const loadCards = async () => {
// Fetch poll results // Fetch poll results
const pollResults = await fetchPollResults(cardDataResponse.poll); const pollResults = await fetchPollResults(cardDataResponse.poll);
const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML // 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); replaceSkeleton(card.identifier, finalCardHTML);
} catch (error) { } catch (error) {
console.error(`Error processing card ${card.identifier}:`, error); console.error(`Error processing card ${card.identifier}:`, error);
@ -222,6 +226,7 @@ const createSkeletonCardHTML = (cardIdentifier) => {
return ` return `
<div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;"> <div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<div><p style="color:rgb(174, 174, 174)">LOADING CARD...</p></div>
<div style="width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;"></div> <div style="width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;"></div>
<div style="margin-left: 10px;"> <div style="margin-left: 10px;">
<div style="width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;"></div> <div style="width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;"></div>
@ -229,7 +234,7 @@ const createSkeletonCardHTML = (cardIdentifier) => {
</div> </div>
</div> </div>
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<div style="width: 100%; height: 40px; background-color: #eee;"></div> <div style="width: 100%; height: 80px; background-color: #eee; color:rgb(17, 24, 28); padding: 0.22vh"><p>PLEASE BE PATIENT</p><p style="color: #11121c"> While data loads from QDN...</div>
</div> </div>
</div> </div>
`; `;
@ -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 createModal = async () => {
const modalHTML = ` const modalHTML = `
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;"> <div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;">
@ -563,22 +584,63 @@ const processLink = async (link) => {
if (link.startsWith('qortal://')) { if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/); const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
if (match) { if (match) {
const firstParam = match[1].toUpperCase(); // Convert to uppercase const firstParam = match[1].toUpperCase();
const remainingPath = match[2] || ""; // Rest of the URL const remainingPath = match[2] || "";
// Perform any asynchronous operation if necessary const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined
await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
return `/render/${firstParam}${remainingPath}`; // 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) Satura­tion:
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 ----------------------------------------------- // 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 { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString(); 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) => ` const linksHTML = links.map((link, index) => `
<button onclick="openModal('${link}')"> <button onclick="openModal('${link}')">
${`Link ${index + 1} - ${link}`} ${`Link ${index + 1} - ${link}`}
@ -590,21 +652,21 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculatePollResults(pollResults, minterGroupMembers, minterAdmins) const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculatePollResults(pollResults, minterGroupMembers, minterAdmins)
await createModal() await createModal()
return ` return `
<div class="minter-card"> <div class="minter-card" style="background-color: ${BgColor}">
<div class="minter-card-header"> <div class="minter-card-header">
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;"> ${avatarHtml}
<h3>${creator}</h3> <h3>${creator}</h3>
<p>${header}</p> <p>${header}</p>
</div> </div>
<div class="support-header"><h5>Minter Post:</h5></div> <div class="support-header"><h5>MINTER'S POST</h5></div>
<div class="info"> <div class="info">
${content} ${content}
</div> </div>
<div class="support-header"><h5>Minter Links:</h5></div> <div class="support-header"><h5>MINTER'S LINKS</h5></div>
<div class="info-links"> <div class="info-links">
${linksHTML} ${linksHTML}
</div> </div>
<div class="results-header support-header"><h5>Current Results:</h5></div> <div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
<div class="minter-card-results"> <div class="minter-card-results">
<div class="admin-results"> <div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span> <span class="admin-yes">Admin Yes: ${adminYes}</span>
@ -616,14 +678,18 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
</div> </div>
<div class="total-results"> <div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span> <span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-yes">Weight: ${totalYesWeight}</span>
<span class="total-no">Total No: ${totalNo}</span> <span class="total-no">Total No: ${totalNo}</span>
<span class="total-no">Weight: ${totalNoWeight}</span>
</div> </div>
</div> </div>
<div class="support-header"><h5>Support Minter?</h5></div> <div class="support-header"><h5>SUPPORT</h5><h5 style="color: #ffae42;">${creator}</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">
<div class="actions-buttons"> <div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button> <button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS</button> <button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div> </div>
</div> </div>
@ -632,7 +698,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea> <textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postComment('${cardIdentifier}')">Post Comment</button> <button onclick="postComment('${cardIdentifier}')">Post Comment</button>
</div> </div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p> <p style="font-size: 0.75rem; margin-top: 3vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
</div> </div>
`; `;
} }

View File

@ -550,7 +550,7 @@ const showSuccessNotification = () => {
// Generate unique attachment ID // Generate unique attachment ID
const generateAttachmentID = (room, fileIndex = null) => { 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; return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
}; };
@ -750,7 +750,7 @@ const isMessageNew = (message, mostRecentMessage) => {
const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => { const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
const replyHtml = buildReplyHtml(message, fetchMessages); const replyHtml = buildReplyHtml(message, fetchMessages);
const attachmentHtml = buildAttachmentHtml(message, room); const attachmentHtml = buildAttachmentHtml(message, room);
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`; const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
return ` return `
@ -770,123 +770,126 @@ const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
</div> </div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button> <button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div> </div>
`; `
}; }
const buildReplyHtml = (message, fetchMessages) => { const buildReplyHtml = (message, fetchMessages) => {
if (!message.replyTo) return ""; if (!message.replyTo) return ""
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo); const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
if (!repliedMessage) return ""; if (!repliedMessage) return ""
return ` return `
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;"> <div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div> <div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
<div class="reply-content">${repliedMessage.content}</div> <div class="reply-content">${repliedMessage.content}</div>
</div> </div>
`; `
}; }
const buildAttachmentHtml = (message, room) => { 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) => { const buildSingleAttachmentHtml = (attachment, room) => {
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) { if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`; const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`
return ` return `
<div class="attachment"> <div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/> <img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
</div> </div>
`; `
} else if
(room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
return fetchEncryptedImageHtml(attachment)
} else { } else {
// Non-image attachment
return ` return `
<div class="attachment"> <div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')"> <button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
Download ${attachment.filename} Download ${attachment.filename}
</button> </button>
</div> </div>
`; `
} }
}; }
const scrollToNewMessages = (firstNewMessageIdentifier) => { const scrollToNewMessages = (firstNewMessageIdentifier) => {
const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`); const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`)
if (newMessageElement) { if (newMessageElement) {
newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
} }
}; }
const updateLatestMessageIdentifiers = (room, mostRecentMessage) => { const updateLatestMessageIdentifiers = (room, mostRecentMessage) => {
latestMessageIdentifiers[room] = mostRecentMessage; latestMessageIdentifiers[room] = mostRecentMessage
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers)); localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers))
}; }
const handleReplyLogic = (fetchMessages) => { const handleReplyLogic = (fetchMessages) => {
const replyButtons = document.querySelectorAll(".reply-button"); const replyButtons = document.querySelectorAll(".reply-button")
replyButtons.forEach(button => { replyButtons.forEach(button => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const replyToMessageIdentifier = button.dataset.messageIdentifier; const replyToMessageIdentifier = button.dataset.messageIdentifier
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier); const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier)
if (repliedMessage) { if (repliedMessage) {
showReplyPreview(repliedMessage); showReplyPreview(repliedMessage)
} }
}); })
}); })
}; }
const showReplyPreview = (repliedMessage) => { const showReplyPreview = (repliedMessage) => {
replyToMessageIdentifier = repliedMessage.identifier; replyToMessageIdentifier = repliedMessage.identifier
const replyContainer = document.createElement("div"); const replyContainer = document.createElement("div")
replyContainer.className = "reply-container"; replyContainer.className = "reply-container"
replyContainer.innerHTML = ` replyContainer.innerHTML = `
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;"> <div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
<strong>Replying to:</strong> ${repliedMessage.content} <strong>Replying to:</strong> ${repliedMessage.content}
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button> <button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
</div> </div>
`; `
if (!document.querySelector(".reply-container")) { if (!document.querySelector(".reply-container")) {
const messageInputSection = document.querySelector(".message-input-section"); const messageInputSection = document.querySelector(".message-input-section")
if (messageInputSection) { if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild); messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild)
document.getElementById("cancel-reply").addEventListener("click", () => { document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null; replyToMessageIdentifier = null
replyContainer.remove(); replyContainer.remove()
}); })
} }
} }
const messageInputSection = document.querySelector(".message-input-section"); const messageInputSection = document.querySelector(".message-input-section")
const editor = document.querySelector(".ql-editor"); const editor = document.querySelector(".ql-editor")
if (messageInputSection) { if (messageInputSection) {
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' })
} }
if (editor) { if (editor) {
editor.focus(); editor.focus()
} }
}; }
const updatePaginationControls = async (room, limit) => { const updatePaginationControls = async (room, limit) => {
const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room) const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room)
renderPaginationControls(room, totalMessages, limit); renderPaginationControls(room, totalMessages, limit)
}; }
// Polling function to check for new messages without clearing existing ones // Polling function to check for new messages without clearing existing ones
function startPollingForNewMessages() { function startPollingForNewMessages() {
setInterval(async () => { setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0]; const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0]
if (activeRoom) { if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage, true); await loadMessagesFromQDN(activeRoom, currentPage, true)
} }
}, 40000); }, 40000)
} }

View File

@ -28,9 +28,20 @@ const uid = async () => {
console.log('Generated uid:', result); console.log('Generated uid:', result);
return 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 // Turn a unix timestamp into a human-readable date
const timestampToHumanReadableDate = async(timestamp) => { const timestampToHumanReadableDate = async(timestamp) => {
console.log('timestampToHumanReadableDate called');
const date = new Date(timestamp); const date = new Date(timestamp);
const day = date.getDate(); const day = date.getDate();
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
@ -45,7 +56,6 @@ const timestampToHumanReadableDate = async(timestamp) => {
}; };
// Base64 encode a string // Base64 encode a string
const base64EncodeString = async (str) => { const base64EncodeString = async (str) => {
console.log('base64EncodeString called');
const encodedString = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(str).buffer))); const encodedString = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(str).buffer)));
console.log('Encoded string:', encodedString); console.log('Encoded string:', encodedString);
return encodedString; return encodedString;
@ -119,7 +129,6 @@ const userState = {
// USER-RELATED QORTAL CALLS ------------------------------------------ // USER-RELATED QORTAL CALLS ------------------------------------------
// Obtain the address of the authenticated user checking userState.accountAddress first. // Obtain the address of the authenticated user checking userState.accountAddress first.
const getUserAddress = async () => { const getUserAddress = async () => {
console.log('getUserAddress called');
try { try {
if (userState.accountAddress) { if (userState.accountAddress) {
console.log('User address found in state:', 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) { async function loadImageHtml(service, name, identifier, filename, mimeType) {
try { try {
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`; const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
// Fetch the file as a blob // Fetch the file as a blob
const response = await fetch(url); const response = await fetch(url);
// Convert the response to a Blob // Convert the response to a Blob
const fileBlob = new Blob([response], { type: mimeType }); const fileBlob = new Blob([response], { type: mimeType });
// Create an Object URL from the Blob // Create an Object URL from the Blob
const objectUrl = URL.createObjectURL(fileBlob); const objectUrl = URL.createObjectURL(fileBlob);
// Use the Object URL as the image source // Use the Object URL as the image source
const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`; const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
return attachmentHtml; return attachmentHtml;
} catch (error) { } 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 fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`; try {
if ((service === "FILE_PRIVATE") || (service === "MAIL_PRIVATE")) { if (!filename || !mimeType) {
service = "FILE_PRIVATE" || service console.error("Filename and mimeType are required");
try{ return;
const encryptedBase64Data = await fetchFileBase64(service, name, identifier) }
const decryptedBase64 = await decryptObject(encryptedBase64Data) let url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?async=true&attempts=5`
const fileBlob = new Blob([decryptedBase64], { type: mimeType });
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({ await qortalRequest({
action: "SAVE_FILE", action: "SAVE_FILE",
blob: fileBlob, blob: fileBlob,
filename, filename,
mimeType mimeType,
}); });
console.log("Encrypted file saved successfully:", filename)
}catch (error) { } else {
console.error("Error fetching ro saving encrypted attachment",error)
} const response = await fetch(url, {
}else{ method: 'GET',
try { headers: {'accept': 'text/plain'}
const response = await fetch(url); });
const blob = await response.blob(); if (!response.ok) {
await qortalRequest({ throw new Error(`File not found (HTTP ${response.status}): ${url}`)
action: "SAVE_FILE", }
blob,
filename: filename, const blob = await response.blob()
mimeType await qortalRequest({
}); action: "SAVE_FILE",
} catch (error) { blob,
console.error("Error fetching or saving the attachment:", error); 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 = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
return attachmentHtml
} }
const renderData = async (service, name, identifier) => { const renderData = async (service, name, identifier) => {
console.log('renderData called'); console.log('renderData called');
console.log('service:', service); console.log('service:', service);